sitevision-cli 0.0.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 (54) hide show
  1. package/dist/app.d.ts +7 -0
  2. package/dist/app.js +180 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +95 -0
  5. package/dist/commands/build.d.ts +12 -0
  6. package/dist/commands/build.js +168 -0
  7. package/dist/commands/deploy.d.ts +17 -0
  8. package/dist/commands/deploy.js +162 -0
  9. package/dist/commands/dev.d.ts +15 -0
  10. package/dist/commands/dev.js +291 -0
  11. package/dist/commands/index.d.ts +4 -0
  12. package/dist/commands/index.js +20 -0
  13. package/dist/commands/info.d.ts +2 -0
  14. package/dist/commands/info.js +66 -0
  15. package/dist/commands/setup-signing.d.ts +2 -0
  16. package/dist/commands/setup-signing.js +82 -0
  17. package/dist/commands/sign.d.ts +14 -0
  18. package/dist/commands/sign.js +103 -0
  19. package/dist/commands/types.d.ts +18 -0
  20. package/dist/commands/types.js +1 -0
  21. package/dist/components/DevPropertiesForm.d.ts +11 -0
  22. package/dist/components/DevPropertiesForm.js +87 -0
  23. package/dist/components/InfoScreen.d.ts +8 -0
  24. package/dist/components/InfoScreen.js +60 -0
  25. package/dist/components/MainMenu.d.ts +8 -0
  26. package/dist/components/MainMenu.js +138 -0
  27. package/dist/components/PasswordInput.d.ts +8 -0
  28. package/dist/components/PasswordInput.js +30 -0
  29. package/dist/components/ProcessOutput.d.ts +7 -0
  30. package/dist/components/ProcessOutput.js +32 -0
  31. package/dist/components/SetupFlow.d.ts +8 -0
  32. package/dist/components/SetupFlow.js +194 -0
  33. package/dist/components/SigningPropertiesForm.d.ts +8 -0
  34. package/dist/components/SigningPropertiesForm.js +49 -0
  35. package/dist/components/StatusIndicator.d.ts +9 -0
  36. package/dist/components/StatusIndicator.js +36 -0
  37. package/dist/components/TextInput.d.ts +11 -0
  38. package/dist/components/TextInput.js +37 -0
  39. package/dist/types/index.d.ts +250 -0
  40. package/dist/types/index.js +6 -0
  41. package/dist/utils/password-prompt.d.ts +4 -0
  42. package/dist/utils/password-prompt.js +45 -0
  43. package/dist/utils/process-runner.d.ts +30 -0
  44. package/dist/utils/process-runner.js +119 -0
  45. package/dist/utils/project-detection.d.ts +103 -0
  46. package/dist/utils/project-detection.js +287 -0
  47. package/dist/utils/sitevision-api.d.ts +56 -0
  48. package/dist/utils/sitevision-api.js +393 -0
  49. package/dist/utils/webpack-runner.d.ts +75 -0
  50. package/dist/utils/webpack-runner.js +313 -0
  51. package/dist/utils/zip.d.ts +64 -0
  52. package/dist/utils/zip.js +246 -0
  53. package/package.json +59 -0
  54. package/readme.md +196 -0
@@ -0,0 +1,393 @@
1
+ /**
2
+ * Sitevision API Client
3
+ *
4
+ * Handles all API interactions with Sitevision servers:
5
+ * - Signing apps via developer.sitevision.se
6
+ * - Deploying to development environments
7
+ * - Deploying to production environments
8
+ * - Creating addons
9
+ * - Activating production apps
10
+ */
11
+ import fs from 'fs';
12
+ import path from 'path';
13
+ import https from 'https';
14
+ import http from 'http';
15
+ import { buildImportEndpointUrl, buildAddonEndpointUrl, } from './project-detection.js';
16
+ // =============================================================================
17
+ // CONSTANTS
18
+ // =============================================================================
19
+ const SIGNING_API_HOST = 'developer.sitevision.se';
20
+ const SIGNING_API_PATH = '/rest-api/appsigner/signapp';
21
+ // =============================================================================
22
+ // UTILITY FUNCTIONS
23
+ // =============================================================================
24
+ /**
25
+ * Create Basic Auth header value
26
+ */
27
+ function createBasicAuth(username, password) {
28
+ return `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`;
29
+ }
30
+ /**
31
+ * Generate a random boundary for multipart form data
32
+ */
33
+ function generateBoundary() {
34
+ return `----FormBoundary${Math.random().toString(36).substring(2)}`;
35
+ }
36
+ /**
37
+ * Create multipart form data for file upload
38
+ */
39
+ function createMultipartFormData(filePath, fieldName, boundary) {
40
+ const filename = path.basename(filePath);
41
+ const fileContent = fs.readFileSync(filePath);
42
+ const parts = [];
43
+ // File part
44
+ parts.push(Buffer.from(`--${boundary}\r\n` +
45
+ `Content-Disposition: form-data; name="${fieldName}"; filename="${filename}"\r\n` +
46
+ `Content-Type: application/octet-stream\r\n\r\n`));
47
+ parts.push(fileContent);
48
+ parts.push(Buffer.from('\r\n'));
49
+ // Closing boundary
50
+ parts.push(Buffer.from(`--${boundary}--\r\n`));
51
+ return {
52
+ body: Buffer.concat(parts),
53
+ contentType: `multipart/form-data; boundary=${boundary}`,
54
+ };
55
+ }
56
+ /**
57
+ * Make an HTTP/HTTPS request
58
+ */
59
+ function makeRequest(url, options) {
60
+ return new Promise((resolve, reject) => {
61
+ const parsedUrl = new URL(url);
62
+ const isHttps = parsedUrl.protocol === 'https:';
63
+ const transport = isHttps ? https : http;
64
+ const headers = {
65
+ ...options.headers,
66
+ };
67
+ if (options.auth) {
68
+ headers['Authorization'] = createBasicAuth(options.auth.username, options.auth.password);
69
+ }
70
+ const requestOptions = {
71
+ hostname: parsedUrl.hostname,
72
+ port: parsedUrl.port || (isHttps ? 443 : 80),
73
+ path: parsedUrl.pathname + parsedUrl.search,
74
+ method: options.method,
75
+ headers,
76
+ };
77
+ const req = transport.request(requestOptions, (res) => {
78
+ const chunks = [];
79
+ res.on('data', (chunk) => {
80
+ chunks.push(chunk);
81
+ });
82
+ res.on('end', () => {
83
+ resolve({
84
+ statusCode: res.statusCode || 0,
85
+ body: Buffer.concat(chunks),
86
+ headers: res.headers,
87
+ });
88
+ });
89
+ });
90
+ req.on('error', reject);
91
+ if (options.body) {
92
+ req.write(options.body);
93
+ }
94
+ req.end();
95
+ });
96
+ }
97
+ // =============================================================================
98
+ // SIGNING API
99
+ // =============================================================================
100
+ /**
101
+ * Sign an app via developer.sitevision.se
102
+ *
103
+ * @param zipPath - Path to the unsigned zip file
104
+ * @param credentials - Signing credentials
105
+ * @param outputPath - Path to write the signed zip file
106
+ */
107
+ export async function signApp(zipPath, credentials, outputPath) {
108
+ // Validate zip exists
109
+ if (!fs.existsSync(zipPath)) {
110
+ return {
111
+ success: false,
112
+ error: `Zip file not found: ${zipPath}. Run build first.`,
113
+ };
114
+ }
115
+ // Build URL with optional certificate name
116
+ let url = `https://${SIGNING_API_HOST}${SIGNING_API_PATH}`;
117
+ if (credentials.certificateName) {
118
+ url += `?certificateName=${encodeURIComponent(credentials.certificateName)}`;
119
+ }
120
+ // Create multipart form data
121
+ const boundary = generateBoundary();
122
+ const { body, contentType } = createMultipartFormData(zipPath, 'file', boundary);
123
+ try {
124
+ const response = await makeRequest(url, {
125
+ method: 'POST',
126
+ headers: {
127
+ 'Content-Type': contentType,
128
+ 'Content-Length': String(body.length),
129
+ },
130
+ body,
131
+ auth: {
132
+ username: credentials.username,
133
+ password: credentials.password,
134
+ },
135
+ });
136
+ if (response.statusCode === 200) {
137
+ // Write signed zip to output path
138
+ const outputDir = path.dirname(outputPath);
139
+ if (!fs.existsSync(outputDir)) {
140
+ fs.mkdirSync(outputDir, { recursive: true });
141
+ }
142
+ fs.writeFileSync(outputPath, response.body);
143
+ return {
144
+ success: true,
145
+ signedFilePath: outputPath,
146
+ };
147
+ }
148
+ if (response.statusCode === 401) {
149
+ return {
150
+ success: false,
151
+ error: 'Unauthorized. Check username and password.',
152
+ };
153
+ }
154
+ return {
155
+ success: false,
156
+ error: `Signing failed with status ${response.statusCode}: ${response.body.toString()}`,
157
+ };
158
+ }
159
+ catch (error) {
160
+ return {
161
+ success: false,
162
+ error: `Signing request failed: ${error instanceof Error ? error.message : String(error)}`,
163
+ };
164
+ }
165
+ }
166
+ // =============================================================================
167
+ // DEPLOYMENT API
168
+ // =============================================================================
169
+ /**
170
+ * Deploy an app to a development environment
171
+ *
172
+ * @param zipPath - Path to the zip file (signed or unsigned)
173
+ * @param config - Deployment configuration
174
+ * @param appType - The app type (web, widget, rest)
175
+ * @param force - Whether to force deploy (overwrite existing)
176
+ */
177
+ export async function deployApp(zipPath, config, appType, force = false) {
178
+ // Validate zip exists
179
+ if (!fs.existsSync(zipPath)) {
180
+ return {
181
+ success: false,
182
+ error: `Zip file not found: ${zipPath}`,
183
+ };
184
+ }
185
+ // Build import URL
186
+ let url = buildImportEndpointUrl(config.domain, config.siteName, config.addonName, appType, config.useHTTP);
187
+ if (force) {
188
+ url += '?force=true';
189
+ }
190
+ // Create multipart form data
191
+ const boundary = generateBoundary();
192
+ const { body, contentType } = createMultipartFormData(zipPath, 'file', boundary);
193
+ try {
194
+ const response = await makeRequest(url, {
195
+ method: 'POST',
196
+ headers: {
197
+ 'Content-Type': contentType,
198
+ 'Content-Length': String(body.length),
199
+ },
200
+ body,
201
+ auth: {
202
+ username: config.username,
203
+ password: config.password,
204
+ },
205
+ });
206
+ if (response.statusCode === 200) {
207
+ // Try to parse response for executable ID
208
+ let executableId;
209
+ try {
210
+ const responseData = JSON.parse(response.body.toString());
211
+ executableId = responseData.executableId || responseData.id;
212
+ }
213
+ catch {
214
+ // Response may not be JSON
215
+ }
216
+ return {
217
+ success: true,
218
+ executableId,
219
+ message: 'Deployment successful',
220
+ };
221
+ }
222
+ if (response.statusCode === 401) {
223
+ return {
224
+ success: false,
225
+ error: 'Unauthorized. Check username and password.',
226
+ };
227
+ }
228
+ if (response.statusCode === 409) {
229
+ return {
230
+ success: false,
231
+ error: 'Conflict. Addon already exists. Use --force to overwrite.',
232
+ };
233
+ }
234
+ return {
235
+ success: false,
236
+ error: `Deployment failed with status ${response.statusCode}: ${response.body.toString()}`,
237
+ };
238
+ }
239
+ catch (error) {
240
+ return {
241
+ success: false,
242
+ error: `Deployment request failed: ${error instanceof Error ? error.message : String(error)}`,
243
+ };
244
+ }
245
+ }
246
+ /**
247
+ * Deploy an app to production
248
+ *
249
+ * @param signedZipPath - Path to the SIGNED zip file (required for production)
250
+ * @param config - Production deployment configuration
251
+ * @param appType - The app type (web, widget, rest)
252
+ */
253
+ export async function deployProduction(signedZipPath, config, appType) {
254
+ // Deploy the signed app
255
+ const deployResult = await deployApp(signedZipPath, config, appType, true);
256
+ if (!deployResult.success) {
257
+ return deployResult;
258
+ }
259
+ // If activation requested and we have an executable ID
260
+ if (config.activate && deployResult.executableId) {
261
+ const activationResult = await activateApp(deployResult.executableId, config, appType);
262
+ if (!activationResult.success) {
263
+ return {
264
+ success: true,
265
+ executableId: deployResult.executableId,
266
+ message: `Deployed successfully but activation failed: ${activationResult.error}`,
267
+ };
268
+ }
269
+ return {
270
+ success: true,
271
+ executableId: deployResult.executableId,
272
+ message: 'Deployed and activated successfully',
273
+ };
274
+ }
275
+ return deployResult;
276
+ }
277
+ // =============================================================================
278
+ // ADDON MANAGEMENT API
279
+ // =============================================================================
280
+ /**
281
+ * Create a new addon on a Sitevision site
282
+ *
283
+ * @param config - Deployment configuration
284
+ * @param appType - The app type (web, widget, rest)
285
+ */
286
+ export async function createAddon(config, appType) {
287
+ const url = buildAddonEndpointUrl(config.domain, config.siteName, appType, config.useHTTP);
288
+ const body = JSON.stringify({
289
+ name: config.addonName,
290
+ category: 'Other',
291
+ });
292
+ try {
293
+ const response = await makeRequest(url, {
294
+ method: 'POST',
295
+ headers: {
296
+ 'Content-Type': 'application/json',
297
+ 'Content-Length': String(Buffer.byteLength(body)),
298
+ },
299
+ body: Buffer.from(body),
300
+ auth: {
301
+ username: config.username,
302
+ password: config.password,
303
+ },
304
+ });
305
+ if (response.statusCode === 200 || response.statusCode === 201) {
306
+ let addonId;
307
+ try {
308
+ const responseData = JSON.parse(response.body.toString());
309
+ addonId = responseData.id;
310
+ }
311
+ catch {
312
+ // Response may not be JSON
313
+ }
314
+ return {
315
+ success: true,
316
+ addonId,
317
+ };
318
+ }
319
+ if (response.statusCode === 401) {
320
+ return {
321
+ success: false,
322
+ error: 'Unauthorized. Check username and password.',
323
+ };
324
+ }
325
+ if (response.statusCode === 409) {
326
+ return {
327
+ success: false,
328
+ error: 'Addon already exists.',
329
+ };
330
+ }
331
+ return {
332
+ success: false,
333
+ error: `Create addon failed with status ${response.statusCode}: ${response.body.toString()}`,
334
+ };
335
+ }
336
+ catch (error) {
337
+ return {
338
+ success: false,
339
+ error: `Create addon request failed: ${error instanceof Error ? error.message : String(error)}`,
340
+ };
341
+ }
342
+ }
343
+ /**
344
+ * Activate a deployed production app
345
+ *
346
+ * @param executableId - The executable ID returned from deployment
347
+ * @param config - Deployment configuration
348
+ * @param appType - The app type (web, widget, rest)
349
+ */
350
+ export async function activateApp(executableId, config, _appType) {
351
+ const protocol = config.useHTTP ? 'http' : 'https';
352
+ const url = `${protocol}://${config.domain}/rest-api/1/0/${encodeURIComponent(config.siteName)}/Addon%20Repository/${encodeURIComponent(config.addonName)}/activateCustomModuleExecutable`;
353
+ const body = JSON.stringify({
354
+ executableId,
355
+ });
356
+ try {
357
+ const response = await makeRequest(url, {
358
+ method: 'PUT',
359
+ headers: {
360
+ 'Content-Type': 'application/json',
361
+ 'Content-Length': String(Buffer.byteLength(body)),
362
+ },
363
+ body: Buffer.from(body),
364
+ auth: {
365
+ username: config.username,
366
+ password: config.password,
367
+ },
368
+ });
369
+ if (response.statusCode === 200) {
370
+ return { success: true };
371
+ }
372
+ if (response.statusCode === 401) {
373
+ return {
374
+ success: false,
375
+ error: 'Unauthorized. Check username and password.',
376
+ };
377
+ }
378
+ return {
379
+ success: false,
380
+ error: `Activation failed with status ${response.statusCode}: ${response.body.toString()}`,
381
+ };
382
+ }
383
+ catch (error) {
384
+ return {
385
+ success: false,
386
+ error: `Activation request failed: ${error instanceof Error ? error.message : String(error)}`,
387
+ };
388
+ }
389
+ }
390
+ // =============================================================================
391
+ // HELPER EXPORTS
392
+ // =============================================================================
393
+ export { createBasicAuth };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Webpack Runner
3
+ *
4
+ * Handles webpack compilation for Sitevision apps.
5
+ * Dynamically loads webpack from the target project's node_modules.
6
+ */
7
+ import { EventEmitter } from 'events';
8
+ import type { BuildOptions, BuildResult } from '../types/index.js';
9
+ /**
10
+ * Events emitted by WebpackRunner:
11
+ * - 'compile': Emitted when compilation starts
12
+ * - 'done': Emitted when compilation completes successfully
13
+ * - 'error': Emitted when compilation fails
14
+ * - 'warning': Emitted when compilation has warnings
15
+ */
16
+ export declare class WebpackRunner extends EventEmitter {
17
+ private projectRoot;
18
+ private options;
19
+ private webpack;
20
+ private config;
21
+ private compiler;
22
+ private watcher;
23
+ constructor(projectRoot: string, options: BuildOptions);
24
+ /**
25
+ * Initialize webpack by loading it from the project's node_modules
26
+ */
27
+ private initialize;
28
+ /**
29
+ * Load webpack configuration from the project
30
+ */
31
+ private loadConfig;
32
+ /**
33
+ * Convert webpack stats to BuildResult
34
+ */
35
+ private statsToResult;
36
+ /**
37
+ * Run a single webpack build
38
+ */
39
+ run(): Promise<BuildResult>;
40
+ /**
41
+ * Start webpack in watch mode
42
+ *
43
+ * @param callback - Called after each compilation
44
+ */
45
+ watch(callback?: (result: BuildResult) => void): Promise<void>;
46
+ /**
47
+ * Stop watching and close the compiler
48
+ */
49
+ close(): Promise<void>;
50
+ /**
51
+ * Check if webpack is available in the project
52
+ */
53
+ static isWebpackAvailable(projectRoot: string): boolean;
54
+ /**
55
+ * Get the webpack version from the project
56
+ */
57
+ static getWebpackVersion(projectRoot: string): string | null;
58
+ }
59
+ /**
60
+ * Run a single webpack build
61
+ *
62
+ * @param projectRoot - Project root directory
63
+ * @param options - Build options
64
+ * @returns Build result
65
+ */
66
+ export declare function runWebpackBuild(projectRoot: string, options: BuildOptions): Promise<BuildResult>;
67
+ /**
68
+ * Start webpack in watch mode
69
+ *
70
+ * @param projectRoot - Project root directory
71
+ * @param options - Build options
72
+ * @param callback - Called after each compilation
73
+ * @returns WebpackRunner instance (call .close() to stop)
74
+ */
75
+ export declare function startWebpackWatch(projectRoot: string, options: BuildOptions, callback?: (result: BuildResult) => void): Promise<WebpackRunner>;