@boltic/cli 1.0.35 ā 1.1.1-dev.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/serverless.js +174 -0
- package/cli.js +8 -0
- package/commands/serverless.js +1958 -0
- package/helper/serverless.js +1717 -0
- package/package.json +3 -1
|
@@ -0,0 +1,1958 @@
|
|
|
1
|
+
import { search, input } from "@inquirer/prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { spawn, execSync } from "child_process";
|
|
6
|
+
|
|
7
|
+
import { getCurrentEnv } from "../helper/env.js";
|
|
8
|
+
import {
|
|
9
|
+
SUPPORTED_LANGUAGES,
|
|
10
|
+
LANGUAGE_VERSIONS,
|
|
11
|
+
HANDLER_MAPPING,
|
|
12
|
+
LANGUAGE_CHOICES,
|
|
13
|
+
REQUIRED_DEPENDENCIES,
|
|
14
|
+
parseCreateArgs,
|
|
15
|
+
parseTestArgs,
|
|
16
|
+
parsePublishArgs,
|
|
17
|
+
createServerlessFiles,
|
|
18
|
+
loadBolticConfig,
|
|
19
|
+
parseLanguageFromConfig,
|
|
20
|
+
parseHandlerConfig,
|
|
21
|
+
detectLanguage,
|
|
22
|
+
generateTestFiles,
|
|
23
|
+
getStartCommand,
|
|
24
|
+
checkNodeDependencies,
|
|
25
|
+
getTestEnvironmentVariables,
|
|
26
|
+
cleanupGeneratedFiles,
|
|
27
|
+
displayTestStartupMessage,
|
|
28
|
+
readHandlerFile,
|
|
29
|
+
buildUpdatePayload,
|
|
30
|
+
displayPublishSuccessMessage,
|
|
31
|
+
createPulledServerlessFiles,
|
|
32
|
+
displayPullSuccessMessage,
|
|
33
|
+
detectHandlerFunctionFromCode,
|
|
34
|
+
pollServerlessStatus,
|
|
35
|
+
} from "../helper/serverless.js";
|
|
36
|
+
import {
|
|
37
|
+
listAllServerless,
|
|
38
|
+
pullServerless,
|
|
39
|
+
publishServerless,
|
|
40
|
+
updateServerless,
|
|
41
|
+
} from "../api/serverless.js";
|
|
42
|
+
|
|
43
|
+
// Define commands and their descriptions
|
|
44
|
+
const commands = {
|
|
45
|
+
create: {
|
|
46
|
+
description: "Create a new serverless function",
|
|
47
|
+
action: handleCreate,
|
|
48
|
+
},
|
|
49
|
+
publish: {
|
|
50
|
+
description: "Publish a serverless",
|
|
51
|
+
action: handlePublish,
|
|
52
|
+
},
|
|
53
|
+
pull: {
|
|
54
|
+
description: "Pull a serverless",
|
|
55
|
+
action: handlePull,
|
|
56
|
+
},
|
|
57
|
+
test: {
|
|
58
|
+
description: "Test a serverless function locally",
|
|
59
|
+
action: handleTest,
|
|
60
|
+
},
|
|
61
|
+
help: {
|
|
62
|
+
description: "Show help for serverless commands",
|
|
63
|
+
action: showHelp,
|
|
64
|
+
},
|
|
65
|
+
list: {
|
|
66
|
+
description: "List all serverless functions",
|
|
67
|
+
action: handleList,
|
|
68
|
+
},
|
|
69
|
+
status: {
|
|
70
|
+
description: "Show status of a serverless function",
|
|
71
|
+
action: handleStatus,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Serverless type choices for dropdown
|
|
76
|
+
const SERVERLESS_TYPE_CHOICES = [
|
|
77
|
+
{ name: "š¦ Git - Deploy from Git repository", value: "git" },
|
|
78
|
+
{ name: "š Blueprint - Write code directly", value: "code" },
|
|
79
|
+
{ name: "š³ Container - Deploy Docker container", value: "container" },
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handle the create serverless command
|
|
84
|
+
*/
|
|
85
|
+
async function handleCreate(args = []) {
|
|
86
|
+
try {
|
|
87
|
+
console.log(
|
|
88
|
+
"\n" +
|
|
89
|
+
chalk.bgCyan.black(" š SERVERLESS CREATE ") +
|
|
90
|
+
chalk.cyan(" Initialize a new serverless function\n")
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
// Step 1: Parse CLI arguments
|
|
94
|
+
const parsedArgs = parseCreateArgs(args);
|
|
95
|
+
let { name, language, directory, type } = parsedArgs;
|
|
96
|
+
|
|
97
|
+
// Step 2: Serverless Type Selection
|
|
98
|
+
if (!type) {
|
|
99
|
+
type = await search({
|
|
100
|
+
message: "Select Serverless Type:",
|
|
101
|
+
source: async (term) => {
|
|
102
|
+
if (!term) return SERVERLESS_TYPE_CHOICES;
|
|
103
|
+
return SERVERLESS_TYPE_CHOICES.filter(
|
|
104
|
+
(choice) =>
|
|
105
|
+
choice.name
|
|
106
|
+
.toLowerCase()
|
|
107
|
+
.includes(term.toLowerCase()) ||
|
|
108
|
+
choice.value
|
|
109
|
+
.toLowerCase()
|
|
110
|
+
.includes(term.toLowerCase())
|
|
111
|
+
);
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
console.log(chalk.cyan("š¦ Selected type: ") + chalk.bold.white(type));
|
|
117
|
+
|
|
118
|
+
// Step 3: Name Input (required - no random generation)
|
|
119
|
+
if (!name) {
|
|
120
|
+
name = await input({
|
|
121
|
+
message: "Enter serverless function name:",
|
|
122
|
+
validate: (value) => {
|
|
123
|
+
if (!value || value.trim() === "") {
|
|
124
|
+
return "Name is required";
|
|
125
|
+
}
|
|
126
|
+
// Validate name format (alphanumeric, hyphens, underscores)
|
|
127
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(value.trim())) {
|
|
128
|
+
return "Name must start with a letter and contain only letters, numbers, hyphens, and underscores";
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
name = name.trim();
|
|
134
|
+
}
|
|
135
|
+
console.log(
|
|
136
|
+
chalk.cyan("š Serverless name: ") + chalk.bold.white(name)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Step 4: Language Selection (skip for container type)
|
|
140
|
+
let version = null;
|
|
141
|
+
if (type !== "container") {
|
|
142
|
+
if (!language) {
|
|
143
|
+
language = await search({
|
|
144
|
+
message: "Select Language:",
|
|
145
|
+
source: async (term) => {
|
|
146
|
+
if (!term) return LANGUAGE_CHOICES;
|
|
147
|
+
return LANGUAGE_CHOICES.filter(
|
|
148
|
+
(choice) =>
|
|
149
|
+
choice.name
|
|
150
|
+
.toLowerCase()
|
|
151
|
+
.includes(term.toLowerCase()) ||
|
|
152
|
+
choice.value
|
|
153
|
+
.toLowerCase()
|
|
154
|
+
.includes(term.toLowerCase())
|
|
155
|
+
);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
} else {
|
|
159
|
+
// Validate the provided language
|
|
160
|
+
if (!SUPPORTED_LANGUAGES.includes(language)) {
|
|
161
|
+
console.error(
|
|
162
|
+
chalk.red(`\nā Unsupported language: ${language}`)
|
|
163
|
+
);
|
|
164
|
+
console.log(
|
|
165
|
+
chalk.yellow(
|
|
166
|
+
`Supported languages: ${SUPPORTED_LANGUAGES.join(", ")}`
|
|
167
|
+
)
|
|
168
|
+
);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Step 5: Get latest language version
|
|
174
|
+
version = LANGUAGE_VERSIONS[language];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Step 6: Determine target directory
|
|
178
|
+
const targetDir = path.join(directory, name);
|
|
179
|
+
|
|
180
|
+
// Check if directory already exists
|
|
181
|
+
if (fs.existsSync(targetDir)) {
|
|
182
|
+
console.error(
|
|
183
|
+
chalk.red(`\nā Directory already exists: ${targetDir}`)
|
|
184
|
+
);
|
|
185
|
+
console.log(
|
|
186
|
+
chalk.yellow(
|
|
187
|
+
"Please choose a different name or delete the existing directory."
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Create the target directory
|
|
194
|
+
try {
|
|
195
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
196
|
+
} catch (err) {
|
|
197
|
+
console.error(
|
|
198
|
+
chalk.red(`\nā Failed to create directory: ${targetDir}`)
|
|
199
|
+
);
|
|
200
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Branch based on type
|
|
205
|
+
if (type === "git") {
|
|
206
|
+
// For git type: create empty folder with boltic-properties.yaml only
|
|
207
|
+
await handleGitTypeCreate(name, language, version, targetDir);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (type === "container") {
|
|
212
|
+
// For container type: ask for image and create serverless
|
|
213
|
+
await handleContainerTypeCreate(name, targetDir);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// For code type: create full template files and call create API
|
|
218
|
+
await handleCodeTypeCreate(name, language, version, targetDir);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
if (
|
|
221
|
+
error.message &&
|
|
222
|
+
error.message.includes("User force closed the prompt")
|
|
223
|
+
) {
|
|
224
|
+
console.log(chalk.yellow("\nā ļø Operation cancelled by user"));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
// Handle other errors
|
|
228
|
+
console.error(
|
|
229
|
+
chalk.red("\nā An error occurred:"),
|
|
230
|
+
error.message || "Unknown error"
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Handle code type serverless creation - creates folder with template files and calls create API
|
|
237
|
+
*/
|
|
238
|
+
async function handleCodeTypeCreate(name, language, version, targetDir) {
|
|
239
|
+
const templateContext = {
|
|
240
|
+
AppSlug: name,
|
|
241
|
+
Language: `${language}/${version}`,
|
|
242
|
+
Region: "asia-south1",
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
console.log(chalk.cyan("\nš Creating serverless function files..."));
|
|
246
|
+
console.log(chalk.dim(` Type: code`));
|
|
247
|
+
console.log(chalk.dim(` Language: ${language}/${version}`));
|
|
248
|
+
console.log(chalk.dim(` Region: ${templateContext.Region}`));
|
|
249
|
+
console.log(chalk.dim(` Handler: ${HANDLER_MAPPING[language]}`));
|
|
250
|
+
|
|
251
|
+
// Create template files
|
|
252
|
+
try {
|
|
253
|
+
createServerlessFiles(targetDir, language, templateContext);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
console.error(chalk.red(`\nā Failed to create template files`));
|
|
256
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
257
|
+
// Cleanup
|
|
258
|
+
try {
|
|
259
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
260
|
+
} catch {
|
|
261
|
+
// Ignore cleanup errors
|
|
262
|
+
}
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Get authentication credentials
|
|
267
|
+
const env = await getCurrentEnv();
|
|
268
|
+
if (!env || !env.token || !env.session) {
|
|
269
|
+
console.error(chalk.red("\nā Not authenticated. Please login first."));
|
|
270
|
+
console.log(chalk.yellow(" Run: boltic login"));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const { apiUrl, token, accountId, session } = env;
|
|
275
|
+
|
|
276
|
+
// Read the handler file to get the code
|
|
277
|
+
const handlerFileName = HANDLER_MAPPING[language].split(".")[0];
|
|
278
|
+
let handlerFile;
|
|
279
|
+
if (language === "java") {
|
|
280
|
+
handlerFile = path.join(
|
|
281
|
+
targetDir,
|
|
282
|
+
"src",
|
|
283
|
+
"main",
|
|
284
|
+
"java",
|
|
285
|
+
"com",
|
|
286
|
+
"boltic",
|
|
287
|
+
"io",
|
|
288
|
+
"serverless",
|
|
289
|
+
"Handler.java"
|
|
290
|
+
);
|
|
291
|
+
} else if (language === "golang") {
|
|
292
|
+
handlerFile = path.join(targetDir, `${handlerFileName}.go`);
|
|
293
|
+
} else if (language === "python") {
|
|
294
|
+
handlerFile = path.join(targetDir, `${handlerFileName}.py`);
|
|
295
|
+
} else {
|
|
296
|
+
handlerFile = path.join(targetDir, `${handlerFileName}.js`);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const code = fs.readFileSync(handlerFile, "utf-8");
|
|
300
|
+
|
|
301
|
+
// Build the payload for create API
|
|
302
|
+
const payload = {
|
|
303
|
+
Name: name,
|
|
304
|
+
Runtime: "code",
|
|
305
|
+
Env: {},
|
|
306
|
+
PortMap: [],
|
|
307
|
+
Scaling: {
|
|
308
|
+
AutoStop: false,
|
|
309
|
+
Min: 1,
|
|
310
|
+
Max: 1,
|
|
311
|
+
MaxIdleTime: 0,
|
|
312
|
+
},
|
|
313
|
+
Resources: {
|
|
314
|
+
CPU: 0.1,
|
|
315
|
+
MemoryMB: 128,
|
|
316
|
+
MemoryMaxMB: 128,
|
|
317
|
+
},
|
|
318
|
+
CodeOpts: {
|
|
319
|
+
Language: `${language}/${version}`,
|
|
320
|
+
Packages: [],
|
|
321
|
+
Code: code,
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// Call create serverless API
|
|
326
|
+
console.log(chalk.cyan("\nš¤ Creating serverless function..."));
|
|
327
|
+
const response = await publishServerless(apiUrl, token, session, payload);
|
|
328
|
+
|
|
329
|
+
if (!response) {
|
|
330
|
+
console.error(chalk.red("\nā Failed to create serverless function"));
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Update boltic-properties.yaml with serverlessId
|
|
335
|
+
const serverlessId = response.ID || response.data?.ID || response._id;
|
|
336
|
+
if (serverlessId) {
|
|
337
|
+
const bolticYamlPath = path.join(targetDir, "boltic-properties.yaml");
|
|
338
|
+
let bolticYamlContent = fs.readFileSync(bolticYamlPath, "utf-8");
|
|
339
|
+
// Add serverlessId at the top after app line
|
|
340
|
+
bolticYamlContent = bolticYamlContent.replace(
|
|
341
|
+
/^(app: .*)$/m,
|
|
342
|
+
`$1\nserverlessId: "${serverlessId}"`
|
|
343
|
+
);
|
|
344
|
+
fs.writeFileSync(bolticYamlPath, bolticYamlContent);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Display success message
|
|
348
|
+
console.log("\n" + chalk.bgGreen.black(" ā CREATED ") + "\n");
|
|
349
|
+
console.log(
|
|
350
|
+
chalk.green("š Blueprint serverless function created successfully!")
|
|
351
|
+
);
|
|
352
|
+
console.log();
|
|
353
|
+
console.log(chalk.cyan(" Name: ") + chalk.white(name));
|
|
354
|
+
console.log(chalk.cyan(" Type: ") + chalk.white("code"));
|
|
355
|
+
console.log(
|
|
356
|
+
chalk.cyan(" Language: ") + chalk.white(`${language}/${version}`)
|
|
357
|
+
);
|
|
358
|
+
console.log(chalk.cyan(" Location: ") + chalk.white(targetDir));
|
|
359
|
+
if (serverlessId) {
|
|
360
|
+
console.log(
|
|
361
|
+
chalk.cyan(" Serverless ID: ") + chalk.white(serverlessId)
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
console.log();
|
|
365
|
+
|
|
366
|
+
// Poll for serverless status until running
|
|
367
|
+
if (serverlessId) {
|
|
368
|
+
await pollServerlessStatus(pullServerless, serverlessId, {
|
|
369
|
+
apiUrl,
|
|
370
|
+
token,
|
|
371
|
+
accountId,
|
|
372
|
+
session,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
console.log(chalk.yellow("š Next steps:"));
|
|
377
|
+
console.log(chalk.dim(" 1. Edit your handler code"));
|
|
378
|
+
console.log(chalk.dim(" 2. Test locally: boltic serverless test"));
|
|
379
|
+
console.log(chalk.dim(" 3. Update: boltic serverless publish"));
|
|
380
|
+
console.log();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Handle git type serverless creation - creates folder with boltic-properties.yaml and calls create API
|
|
385
|
+
*/
|
|
386
|
+
async function handleGitTypeCreate(name, language, version, targetDir) {
|
|
387
|
+
console.log(chalk.cyan("\nš Creating git-based serverless project..."));
|
|
388
|
+
console.log(chalk.dim(` Type: git`));
|
|
389
|
+
console.log(chalk.dim(` Language: ${language}/${version}`));
|
|
390
|
+
|
|
391
|
+
// Get authentication credentials first
|
|
392
|
+
const env = await getCurrentEnv();
|
|
393
|
+
if (!env || !env.token || !env.session) {
|
|
394
|
+
console.error(chalk.red("\nā Not authenticated. Please login first."));
|
|
395
|
+
console.log(chalk.yellow(" Run: boltic login"));
|
|
396
|
+
// Cleanup the created directory
|
|
397
|
+
try {
|
|
398
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
399
|
+
} catch {
|
|
400
|
+
// Ignore cleanup errors
|
|
401
|
+
}
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const { apiUrl, token, session } = env;
|
|
406
|
+
|
|
407
|
+
// Build the payload for git type
|
|
408
|
+
const payload = {
|
|
409
|
+
Name: name,
|
|
410
|
+
Runtime: "git",
|
|
411
|
+
Env: {},
|
|
412
|
+
PortMap: [],
|
|
413
|
+
Scaling: {
|
|
414
|
+
AutoStop: false,
|
|
415
|
+
Min: 1,
|
|
416
|
+
Max: 1,
|
|
417
|
+
MaxIdleTime: 0,
|
|
418
|
+
},
|
|
419
|
+
Resources: {
|
|
420
|
+
CPU: 0.1,
|
|
421
|
+
MemoryMB: 128,
|
|
422
|
+
MemoryMaxMB: 128,
|
|
423
|
+
},
|
|
424
|
+
CodeOpts: {
|
|
425
|
+
Language: `${language}/${version}`,
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// Call create serverless API
|
|
430
|
+
console.log(chalk.cyan("\nš¤ Creating git-based serverless function..."));
|
|
431
|
+
const response = await publishServerless(apiUrl, token, session, payload);
|
|
432
|
+
|
|
433
|
+
if (!response) {
|
|
434
|
+
console.error(chalk.red("\nā Failed to create serverless function"));
|
|
435
|
+
// Cleanup the created directory
|
|
436
|
+
try {
|
|
437
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
438
|
+
} catch {
|
|
439
|
+
// Ignore cleanup errors
|
|
440
|
+
}
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Extract serverless ID and git info from response
|
|
445
|
+
// Response structure: { ID, Links: { Git: { Repository: { SshURL, HtmlURL, CloneURL, ... } } } }
|
|
446
|
+
const serverlessId = response.ID || response.data?.ID || response._id;
|
|
447
|
+
const gitRepo =
|
|
448
|
+
response.Links?.Git?.Repository ||
|
|
449
|
+
response.data?.Links?.Git?.Repository;
|
|
450
|
+
const gitSshUrl = gitRepo?.SshURL || "";
|
|
451
|
+
const gitHttpUrl = gitRepo?.HtmlURL || "";
|
|
452
|
+
const gitCloneUrl = gitRepo?.CloneURL || "";
|
|
453
|
+
|
|
454
|
+
// Create boltic-properties.yaml with serverlessId
|
|
455
|
+
const bolticYamlContent = `app: "${name}"
|
|
456
|
+
serverlessId: "${serverlessId}"
|
|
457
|
+
region: "asia-south1"
|
|
458
|
+
handler: "${HANDLER_MAPPING[language]}"
|
|
459
|
+
language: "${language}/${version}"
|
|
460
|
+
|
|
461
|
+
serverlessConfig:
|
|
462
|
+
Name: "${name}"
|
|
463
|
+
Description: ""
|
|
464
|
+
Runtime: "git"
|
|
465
|
+
# Environment variables for your serverless function
|
|
466
|
+
# To add env variables, replace {} with key-value pairs like:
|
|
467
|
+
# Env:
|
|
468
|
+
# API_KEY: "your-api-key"
|
|
469
|
+
#TO add port map, replace {} with port map like:
|
|
470
|
+
# PortMap:
|
|
471
|
+
# - Name: "port"
|
|
472
|
+
# Port: "8080"
|
|
473
|
+
# Protocol: "http"/"https"
|
|
474
|
+
Env: {}
|
|
475
|
+
PortMap: {}
|
|
476
|
+
Scaling:
|
|
477
|
+
AutoStop: false
|
|
478
|
+
Min: 1
|
|
479
|
+
Max: 1
|
|
480
|
+
MaxIdleTime: 300
|
|
481
|
+
Resources:
|
|
482
|
+
CPU: 0.1
|
|
483
|
+
MemoryMB: 128
|
|
484
|
+
MemoryMaxMB: 128
|
|
485
|
+
Timeout: 60
|
|
486
|
+
Validations: null
|
|
487
|
+
|
|
488
|
+
build:
|
|
489
|
+
builtin: dockerfile
|
|
490
|
+
ignorefile: .gitignore
|
|
491
|
+
`;
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
fs.writeFileSync(
|
|
495
|
+
path.join(targetDir, "boltic-properties.yaml"),
|
|
496
|
+
bolticYamlContent
|
|
497
|
+
);
|
|
498
|
+
} catch (err) {
|
|
499
|
+
console.error(
|
|
500
|
+
chalk.red(`\nā Failed to create boltic-properties.yaml`)
|
|
501
|
+
);
|
|
502
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// Check if user has git access by trying ls-remote
|
|
507
|
+
let hasGitAccess = false;
|
|
508
|
+
if (gitSshUrl) {
|
|
509
|
+
console.log(chalk.cyan("\nš Checking git repository access..."));
|
|
510
|
+
try {
|
|
511
|
+
// Initialize git repo
|
|
512
|
+
execSync(`git init`, { cwd: targetDir, stdio: "pipe" });
|
|
513
|
+
execSync(`git remote add origin ${gitSshUrl}`, {
|
|
514
|
+
cwd: targetDir,
|
|
515
|
+
stdio: "pipe",
|
|
516
|
+
});
|
|
517
|
+
// Try ls-remote to check SSH access
|
|
518
|
+
execSync(`git ls-remote ${gitSshUrl}`, {
|
|
519
|
+
cwd: targetDir,
|
|
520
|
+
stdio: "pipe",
|
|
521
|
+
timeout: 15000,
|
|
522
|
+
});
|
|
523
|
+
hasGitAccess = true;
|
|
524
|
+
} catch (err) {
|
|
525
|
+
hasGitAccess = false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// If user has access, create main branch
|
|
530
|
+
if (hasGitAccess) {
|
|
531
|
+
try {
|
|
532
|
+
console.log(chalk.cyan("š§ Setting up git branch..."));
|
|
533
|
+
// Create main branch
|
|
534
|
+
execSync(`git checkout -b main`, { cwd: targetDir, stdio: "pipe" });
|
|
535
|
+
console.log(chalk.green("ā Created main branch"));
|
|
536
|
+
} catch (err) {
|
|
537
|
+
// Ignore errors in branch setup, user can do it manually
|
|
538
|
+
console.log(
|
|
539
|
+
chalk.yellow(
|
|
540
|
+
"ā ļø Could not auto-setup git branch. You can set it up manually."
|
|
541
|
+
)
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Display success message
|
|
547
|
+
console.log("\n" + chalk.bgGreen.black(" ā CREATED ") + "\n");
|
|
548
|
+
console.log(
|
|
549
|
+
chalk.green("š Git-based serverless project created successfully!")
|
|
550
|
+
);
|
|
551
|
+
console.log();
|
|
552
|
+
console.log(chalk.cyan(" Name: ") + chalk.white(name));
|
|
553
|
+
console.log(chalk.cyan(" Type: ") + chalk.white("git"));
|
|
554
|
+
console.log(
|
|
555
|
+
chalk.cyan(" Language: ") + chalk.white(`${language}/${version}`)
|
|
556
|
+
);
|
|
557
|
+
console.log(chalk.cyan(" Location: ") + chalk.white(targetDir));
|
|
558
|
+
console.log(chalk.cyan(" Serverless ID: ") + chalk.white(serverlessId));
|
|
559
|
+
|
|
560
|
+
if (gitSshUrl || gitHttpUrl) {
|
|
561
|
+
console.log();
|
|
562
|
+
console.log(chalk.cyan(" š¦ Git Repository:"));
|
|
563
|
+
if (gitSshUrl) {
|
|
564
|
+
console.log(chalk.cyan(" SSH URL: ") + chalk.white(gitSshUrl));
|
|
565
|
+
}
|
|
566
|
+
if (gitHttpUrl) {
|
|
567
|
+
console.log(
|
|
568
|
+
chalk.cyan(" Web URL: ") + chalk.white(gitHttpUrl)
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
if (gitCloneUrl) {
|
|
572
|
+
console.log(
|
|
573
|
+
chalk.cyan(" Clone URL: ") + chalk.white(gitCloneUrl)
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
console.log();
|
|
577
|
+
|
|
578
|
+
if (hasGitAccess) {
|
|
579
|
+
console.log(
|
|
580
|
+
chalk.green("ā
You have access to the git repository!")
|
|
581
|
+
);
|
|
582
|
+
console.log(chalk.green("ā
Main branch created!"));
|
|
583
|
+
console.log();
|
|
584
|
+
console.log(
|
|
585
|
+
chalk.yellow("š Next steps - Add your code and push:")
|
|
586
|
+
);
|
|
587
|
+
console.log(chalk.dim(" 1. Add your server code to this folder"));
|
|
588
|
+
console.log(chalk.dim(" 2. Commit and push:"));
|
|
589
|
+
console.log(chalk.white(` git add .`));
|
|
590
|
+
console.log(chalk.white(` git commit -m "Initial commit"`));
|
|
591
|
+
console.log(chalk.white(` git push -u origin main`));
|
|
592
|
+
} else {
|
|
593
|
+
console.log(
|
|
594
|
+
chalk.red("ā You don't have access to this git repository.")
|
|
595
|
+
);
|
|
596
|
+
console.log(
|
|
597
|
+
chalk.yellow(
|
|
598
|
+
" Please add your SSH key from the Boltic UI to get access."
|
|
599
|
+
)
|
|
600
|
+
);
|
|
601
|
+
console.log();
|
|
602
|
+
console.log(
|
|
603
|
+
chalk.yellow("š Once you have access, push your code:")
|
|
604
|
+
);
|
|
605
|
+
console.log(chalk.dim(" 1. Add your code to this folder"));
|
|
606
|
+
console.log(chalk.dim(" 2. Run:"));
|
|
607
|
+
console.log(chalk.white(` git checkout -b main`));
|
|
608
|
+
console.log(chalk.white(` git add .`));
|
|
609
|
+
console.log(chalk.white(` git commit -m "Initial commit"`));
|
|
610
|
+
console.log(chalk.white(` git push -u origin main`));
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
console.log();
|
|
614
|
+
console.log(chalk.yellow("š Next steps:"));
|
|
615
|
+
console.log(chalk.dim(" 1. Add your code to this folder"));
|
|
616
|
+
console.log(chalk.dim(" 2. Configure git remote and push your code"));
|
|
617
|
+
}
|
|
618
|
+
console.log();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Handle container type serverless creation - creates empty folder with boltic-properties.yaml
|
|
623
|
+
*/
|
|
624
|
+
async function handleContainerTypeCreate(name, targetDir) {
|
|
625
|
+
console.log(
|
|
626
|
+
chalk.cyan("\nš³ Creating container-based serverless project...")
|
|
627
|
+
);
|
|
628
|
+
console.log(chalk.dim(` Type: container`));
|
|
629
|
+
|
|
630
|
+
// Ask for container image URI
|
|
631
|
+
const containerImage = await input({
|
|
632
|
+
message: "Enter container image URI (e.g., docker.io/user/image:tag):",
|
|
633
|
+
validate: (value) => {
|
|
634
|
+
if (!value || value.trim() === "") {
|
|
635
|
+
return "Container image URI is required";
|
|
636
|
+
}
|
|
637
|
+
return true;
|
|
638
|
+
},
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
console.log(chalk.cyan("\nš¤ Creating serverless function..."));
|
|
642
|
+
|
|
643
|
+
// Get auth credentials
|
|
644
|
+
const { apiUrl, token, accountId, session } = await getCurrentEnv();
|
|
645
|
+
|
|
646
|
+
// Build create payload for container type
|
|
647
|
+
const createPayload = {
|
|
648
|
+
Name: name,
|
|
649
|
+
Description: "",
|
|
650
|
+
Runtime: "container",
|
|
651
|
+
PortMap: [],
|
|
652
|
+
Scaling: {
|
|
653
|
+
AutoStop: false,
|
|
654
|
+
Min: 1,
|
|
655
|
+
Max: 1,
|
|
656
|
+
MaxIdleTime: 300,
|
|
657
|
+
},
|
|
658
|
+
Resources: {
|
|
659
|
+
CPU: 0.1,
|
|
660
|
+
MemoryMB: 128,
|
|
661
|
+
MemoryMaxMB: 128,
|
|
662
|
+
},
|
|
663
|
+
Timeout: 60,
|
|
664
|
+
Validations: null,
|
|
665
|
+
ContainerOpts: {
|
|
666
|
+
Image: containerImage.trim(),
|
|
667
|
+
Args: [],
|
|
668
|
+
Command: "",
|
|
669
|
+
},
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// Call create serverless API
|
|
673
|
+
const response = await publishServerless(
|
|
674
|
+
apiUrl,
|
|
675
|
+
token,
|
|
676
|
+
session,
|
|
677
|
+
createPayload
|
|
678
|
+
);
|
|
679
|
+
|
|
680
|
+
if (!response || !response.ID) {
|
|
681
|
+
console.error(chalk.red("\nā Failed to create serverless function"));
|
|
682
|
+
// Cleanup directory
|
|
683
|
+
try {
|
|
684
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
685
|
+
} catch {
|
|
686
|
+
// Ignore cleanup errors
|
|
687
|
+
}
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const serverlessId = response.ID;
|
|
692
|
+
|
|
693
|
+
// Create boltic-properties.yaml for container type
|
|
694
|
+
const bolticYamlContent = `app: "${name}"
|
|
695
|
+
region: "asia-south1"
|
|
696
|
+
serverlessId: "${serverlessId}"
|
|
697
|
+
|
|
698
|
+
serverlessConfig:
|
|
699
|
+
Name: "${name}"
|
|
700
|
+
Description: ""
|
|
701
|
+
Runtime: "container"
|
|
702
|
+
# Environment variables for your serverless function
|
|
703
|
+
# To add env variables, replace {} with key-value pairs like:
|
|
704
|
+
# Env:
|
|
705
|
+
# API_KEY: "your-api-key"
|
|
706
|
+
Env: {}
|
|
707
|
+
PortMap: []
|
|
708
|
+
Scaling:
|
|
709
|
+
AutoStop: false
|
|
710
|
+
Min: 1
|
|
711
|
+
Max: 1
|
|
712
|
+
MaxIdleTime: 300
|
|
713
|
+
Resources:
|
|
714
|
+
CPU: 0.1
|
|
715
|
+
MemoryMB: 128
|
|
716
|
+
MemoryMaxMB: 128
|
|
717
|
+
Timeout: 60
|
|
718
|
+
Validations: null
|
|
719
|
+
ContainerOpts:
|
|
720
|
+
Image: "${containerImage.trim()}"
|
|
721
|
+
Args: []
|
|
722
|
+
Command: ""
|
|
723
|
+
|
|
724
|
+
build:
|
|
725
|
+
builtin: dockerfile
|
|
726
|
+
ignorefile: .gitignore
|
|
727
|
+
`;
|
|
728
|
+
|
|
729
|
+
try {
|
|
730
|
+
fs.writeFileSync(
|
|
731
|
+
path.join(targetDir, "boltic-properties.yaml"),
|
|
732
|
+
bolticYamlContent
|
|
733
|
+
);
|
|
734
|
+
} catch (err) {
|
|
735
|
+
console.error(
|
|
736
|
+
chalk.red(`\nā Failed to create boltic-properties.yaml`)
|
|
737
|
+
);
|
|
738
|
+
console.error(chalk.red(`Error: ${err.message}`));
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Display success message for container type
|
|
743
|
+
console.log("\n" + chalk.bgGreen.black(" ā CREATED ") + "\n");
|
|
744
|
+
console.log(
|
|
745
|
+
chalk.green(
|
|
746
|
+
"š³ Container-based serverless project created successfully!"
|
|
747
|
+
)
|
|
748
|
+
);
|
|
749
|
+
console.log();
|
|
750
|
+
console.log(chalk.cyan(" Name: ") + chalk.white(name));
|
|
751
|
+
console.log(chalk.cyan(" Type: ") + chalk.white("container"));
|
|
752
|
+
console.log(chalk.cyan(" Image: ") + chalk.white(containerImage.trim()));
|
|
753
|
+
console.log(chalk.cyan(" Location: ") + chalk.white(targetDir));
|
|
754
|
+
console.log(chalk.cyan(" Serverless ID: ") + chalk.white(serverlessId));
|
|
755
|
+
console.log();
|
|
756
|
+
|
|
757
|
+
// Poll for serverless status until running
|
|
758
|
+
await pollServerlessStatus(pullServerless, serverlessId, {
|
|
759
|
+
apiUrl,
|
|
760
|
+
token,
|
|
761
|
+
accountId,
|
|
762
|
+
session,
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
console.log(chalk.yellow("š Next steps:"));
|
|
766
|
+
console.log(
|
|
767
|
+
chalk.dim(" 1. To update configuration, edit boltic-properties.yaml")
|
|
768
|
+
);
|
|
769
|
+
console.log(
|
|
770
|
+
chalk.dim(" 2. To publish changes: boltic serverless publish")
|
|
771
|
+
);
|
|
772
|
+
console.log();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Handle the publish serverless command
|
|
777
|
+
*/
|
|
778
|
+
async function handlePublish(args = []) {
|
|
779
|
+
try {
|
|
780
|
+
console.log(
|
|
781
|
+
"\n" +
|
|
782
|
+
chalk.bgMagenta.black(" š SERVERLESS PUBLISH ") +
|
|
783
|
+
chalk.magenta(" Deploy your serverless function\n")
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
// Step 1: Parse CLI arguments
|
|
787
|
+
const parsedArgs = parsePublishArgs(args);
|
|
788
|
+
const { directory } = parsedArgs;
|
|
789
|
+
|
|
790
|
+
// Validate directory exists
|
|
791
|
+
if (!fs.existsSync(directory)) {
|
|
792
|
+
console.error(
|
|
793
|
+
chalk.red(`\nā Directory does not exist: ${directory}`)
|
|
794
|
+
);
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Step 2: Load boltic-properties.yaml config
|
|
799
|
+
const config = loadBolticConfig(directory);
|
|
800
|
+
if (!config) {
|
|
801
|
+
console.error(
|
|
802
|
+
chalk.red(
|
|
803
|
+
"\nā boltic-properties.yaml not found in the directory"
|
|
804
|
+
)
|
|
805
|
+
);
|
|
806
|
+
console.log(
|
|
807
|
+
chalk.yellow(
|
|
808
|
+
"Please run this command from a serverless project directory."
|
|
809
|
+
)
|
|
810
|
+
);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Step 3: Get app name and language from config
|
|
815
|
+
const appName = config.app;
|
|
816
|
+
const language = config.language; // e.g., "nodejs/20"
|
|
817
|
+
const serverlessId = config.serverlessId;
|
|
818
|
+
const serverlessConfig = config.serverlessConfig;
|
|
819
|
+
|
|
820
|
+
if (!appName) {
|
|
821
|
+
console.error(
|
|
822
|
+
chalk.red("\nā App name not found in boltic-properties.yaml")
|
|
823
|
+
);
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (!language && serverlessConfig?.Runtime !== "container") {
|
|
828
|
+
console.error(
|
|
829
|
+
chalk.red("\nā Language not found in boltic-properties.yaml")
|
|
830
|
+
);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
console.log(chalk.cyan("š App Name: ") + chalk.white(appName));
|
|
835
|
+
console.log(chalk.cyan("š Language: ") + chalk.white(language));
|
|
836
|
+
console.log(
|
|
837
|
+
chalk.cyan("š Runtime: ") +
|
|
838
|
+
chalk.white(serverlessConfig?.Runtime || "code")
|
|
839
|
+
);
|
|
840
|
+
|
|
841
|
+
// Step 4: Read handler file (only for "code" runtime type)
|
|
842
|
+
const languageBase = parseLanguageFromConfig(language);
|
|
843
|
+
const runtime = serverlessConfig?.Runtime || "code";
|
|
844
|
+
let code = null;
|
|
845
|
+
|
|
846
|
+
if (runtime === "code") {
|
|
847
|
+
code = readHandlerFile(directory, languageBase, config);
|
|
848
|
+
|
|
849
|
+
if (!code) {
|
|
850
|
+
console.error(chalk.red("\nā Handler file not found"));
|
|
851
|
+
const handlerConfig = parseHandlerConfig(
|
|
852
|
+
config.handler,
|
|
853
|
+
languageBase
|
|
854
|
+
);
|
|
855
|
+
console.log(
|
|
856
|
+
chalk.yellow(`Expected handler file: ${handlerConfig.file}`)
|
|
857
|
+
);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
console.log(chalk.cyan("š Handler code loaded successfully"));
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Step 5: Get auth credentials
|
|
865
|
+
const { apiUrl, token, accountId, session } = await getCurrentEnv();
|
|
866
|
+
|
|
867
|
+
let response;
|
|
868
|
+
|
|
869
|
+
// Update existing serverless function
|
|
870
|
+
const payload = buildUpdatePayload(serverlessConfig, language, code);
|
|
871
|
+
|
|
872
|
+
console.log(chalk.cyan("\nš¤ Updating serverless function..."));
|
|
873
|
+
response = await updateServerless(
|
|
874
|
+
apiUrl,
|
|
875
|
+
token,
|
|
876
|
+
session,
|
|
877
|
+
serverlessId,
|
|
878
|
+
payload
|
|
879
|
+
);
|
|
880
|
+
|
|
881
|
+
if (response) {
|
|
882
|
+
displayPublishSuccessMessage(appName, response);
|
|
883
|
+
|
|
884
|
+
// Poll for serverless status for code and container types only
|
|
885
|
+
if (runtime === "code" || runtime === "container") {
|
|
886
|
+
await pollServerlessStatus(pullServerless, serverlessId, {
|
|
887
|
+
apiUrl,
|
|
888
|
+
token,
|
|
889
|
+
accountId,
|
|
890
|
+
session,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
} else {
|
|
894
|
+
console.error(
|
|
895
|
+
chalk.red(`\nā Failed to publish serverless function`)
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
} catch (error) {
|
|
899
|
+
if (
|
|
900
|
+
error.message &&
|
|
901
|
+
error.message.includes("User force closed the prompt")
|
|
902
|
+
) {
|
|
903
|
+
console.log(chalk.yellow("\nā ļø Operation cancelled by user"));
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
console.error(
|
|
907
|
+
chalk.red("\nā An error occurred:"),
|
|
908
|
+
error.message || "Unknown error"
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* Handle the test serverless command
|
|
915
|
+
*/
|
|
916
|
+
async function handleTest(args = []) {
|
|
917
|
+
let childProcess = null;
|
|
918
|
+
let language = null;
|
|
919
|
+
let directory = null;
|
|
920
|
+
let retain = false;
|
|
921
|
+
|
|
922
|
+
// Setup cleanup handler
|
|
923
|
+
const cleanup = (signal) => {
|
|
924
|
+
console.log(chalk.yellow(`\n\nā ļø ${signal} received, cleaning up...`));
|
|
925
|
+
|
|
926
|
+
if (childProcess) {
|
|
927
|
+
childProcess.kill("SIGTERM");
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (language && directory) {
|
|
931
|
+
cleanupGeneratedFiles(directory, language, retain);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
process.exit(0);
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
// Register signal handlers
|
|
938
|
+
process.on("SIGINT", () => cleanup("SIGINT"));
|
|
939
|
+
process.on("SIGTERM", () => cleanup("SIGTERM"));
|
|
940
|
+
|
|
941
|
+
try {
|
|
942
|
+
// Step 1: Parse CLI arguments
|
|
943
|
+
const parsedArgs = parseTestArgs(args);
|
|
944
|
+
let {
|
|
945
|
+
port,
|
|
946
|
+
handlerFile,
|
|
947
|
+
handlerFunction,
|
|
948
|
+
command: customCommand,
|
|
949
|
+
} = parsedArgs;
|
|
950
|
+
language = parsedArgs.language;
|
|
951
|
+
directory = parsedArgs.directory;
|
|
952
|
+
retain = parsedArgs.retain;
|
|
953
|
+
|
|
954
|
+
// Validate directory exists
|
|
955
|
+
if (!fs.existsSync(directory)) {
|
|
956
|
+
console.error(
|
|
957
|
+
chalk.red(`\nā Directory does not exist: ${directory}`)
|
|
958
|
+
);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// Step 2: Load boltic-properties.yaml config
|
|
963
|
+
const config = loadBolticConfig(directory);
|
|
964
|
+
if (!config) {
|
|
965
|
+
console.error(
|
|
966
|
+
chalk.red(
|
|
967
|
+
"\nā boltic-properties.yaml not found in the directory"
|
|
968
|
+
)
|
|
969
|
+
);
|
|
970
|
+
console.log(
|
|
971
|
+
chalk.yellow(
|
|
972
|
+
"You can only test code or container type serverless with boltic-properties.yaml"
|
|
973
|
+
)
|
|
974
|
+
);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Check if it's a container type serverless
|
|
979
|
+
const runtime = config.serverlessConfig?.Runtime || "code";
|
|
980
|
+
if (runtime === "container") {
|
|
981
|
+
await handleContainerTest(config, directory, port);
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// For git type, show message that test is not supported
|
|
986
|
+
if (runtime === "git") {
|
|
987
|
+
console.log(
|
|
988
|
+
chalk.yellow(
|
|
989
|
+
"\nā ļø Git type serverless test is not supported via CLI."
|
|
990
|
+
)
|
|
991
|
+
);
|
|
992
|
+
console.log(
|
|
993
|
+
chalk.dim(
|
|
994
|
+
"For git type, run your server directly using your project's start command."
|
|
995
|
+
)
|
|
996
|
+
);
|
|
997
|
+
console.log(
|
|
998
|
+
chalk.dim("Example: npm start, python app.py, go run ., etc.")
|
|
999
|
+
);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Step 3: Determine language (for code type)
|
|
1004
|
+
if (!language && config?.language) {
|
|
1005
|
+
language = parseLanguageFromConfig(config.language);
|
|
1006
|
+
console.log(
|
|
1007
|
+
chalk.cyan("š Using language from boltic-properties.yaml: ") +
|
|
1008
|
+
chalk.bold.white(language)
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (!language) {
|
|
1013
|
+
console.log(
|
|
1014
|
+
chalk.yellow("ā ļø No language specified, auto-detecting...")
|
|
1015
|
+
);
|
|
1016
|
+
language = detectLanguage(directory);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (!language) {
|
|
1020
|
+
console.error(
|
|
1021
|
+
chalk.red(
|
|
1022
|
+
"\nā Could not detect language. Please specify with --language flag."
|
|
1023
|
+
)
|
|
1024
|
+
);
|
|
1025
|
+
console.log(
|
|
1026
|
+
chalk.yellow(
|
|
1027
|
+
`Supported languages: ${SUPPORTED_LANGUAGES.join(", ")}`
|
|
1028
|
+
)
|
|
1029
|
+
);
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// Validate language
|
|
1034
|
+
if (!SUPPORTED_LANGUAGES.includes(language)) {
|
|
1035
|
+
console.error(chalk.red(`\nā Unsupported language: ${language}`));
|
|
1036
|
+
console.log(
|
|
1037
|
+
chalk.yellow(
|
|
1038
|
+
`Supported languages: ${SUPPORTED_LANGUAGES.join(", ")}`
|
|
1039
|
+
)
|
|
1040
|
+
);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Step 4: Determine handler file and function
|
|
1045
|
+
if (!handlerFile || !handlerFunction) {
|
|
1046
|
+
const handlerConfig = parseHandlerConfig(config?.handler, language);
|
|
1047
|
+
handlerFile = handlerFile || handlerConfig.file;
|
|
1048
|
+
handlerFunction = handlerFunction || handlerConfig.function;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Verify handler file exists
|
|
1052
|
+
const handlerPath = path.join(directory, handlerFile);
|
|
1053
|
+
if (!fs.existsSync(handlerPath)) {
|
|
1054
|
+
console.error(
|
|
1055
|
+
chalk.red(`\nā Handler file not found: ${handlerPath}`)
|
|
1056
|
+
);
|
|
1057
|
+
console.log(
|
|
1058
|
+
chalk.yellow(
|
|
1059
|
+
"Please specify the correct handler file with --handler-file flag."
|
|
1060
|
+
)
|
|
1061
|
+
);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Step 4.1: Detect actual handler function name from code
|
|
1066
|
+
// This handles cases where user might have renamed the function (e.g., handler -> handler1)
|
|
1067
|
+
const handlerCode = fs.readFileSync(handlerPath, "utf8");
|
|
1068
|
+
const detectedFunction = detectHandlerFunctionFromCode(
|
|
1069
|
+
handlerCode,
|
|
1070
|
+
language
|
|
1071
|
+
);
|
|
1072
|
+
|
|
1073
|
+
if (detectedFunction && detectedFunction !== handlerFunction) {
|
|
1074
|
+
console.log(
|
|
1075
|
+
chalk.yellow(`ā ļø Detected handler function: `) +
|
|
1076
|
+
chalk.bold.white(detectedFunction) +
|
|
1077
|
+
chalk.yellow(` (config says: ${handlerFunction})`)
|
|
1078
|
+
);
|
|
1079
|
+
console.log(
|
|
1080
|
+
chalk.cyan(" Using detected function name from code...")
|
|
1081
|
+
);
|
|
1082
|
+
handlerFunction = detectedFunction;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
console.log(
|
|
1086
|
+
chalk.cyan("š¦ Handler: ") +
|
|
1087
|
+
chalk.white(`${handlerFile}.${handlerFunction}`)
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
// Step 5: Install dependencies
|
|
1091
|
+
if (language === "nodejs") {
|
|
1092
|
+
const missingDeps = checkNodeDependencies(
|
|
1093
|
+
directory,
|
|
1094
|
+
REQUIRED_DEPENDENCIES.nodejs
|
|
1095
|
+
);
|
|
1096
|
+
|
|
1097
|
+
if (missingDeps.length > 0) {
|
|
1098
|
+
console.log(
|
|
1099
|
+
chalk.yellow(
|
|
1100
|
+
`\nš¦ Missing dependencies: ${missingDeps.join(", ")}`
|
|
1101
|
+
)
|
|
1102
|
+
);
|
|
1103
|
+
console.log(chalk.cyan(" Installing with --no-save..."));
|
|
1104
|
+
|
|
1105
|
+
try {
|
|
1106
|
+
execSync(`npm install ${missingDeps.join(" ")} --no-save`, {
|
|
1107
|
+
cwd: directory,
|
|
1108
|
+
stdio: "inherit",
|
|
1109
|
+
});
|
|
1110
|
+
console.log(chalk.green(" ā Dependencies installed"));
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
console.error(
|
|
1113
|
+
chalk.red("\nā Failed to install dependencies")
|
|
1114
|
+
);
|
|
1115
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// Install Python dependencies using virtual environment
|
|
1122
|
+
if (language === "python") {
|
|
1123
|
+
const venvPath = path.join(directory, ".venv");
|
|
1124
|
+
const venvPython = path.join(venvPath, "bin", "python3");
|
|
1125
|
+
const venvPip = path.join(venvPath, "bin", "pip3");
|
|
1126
|
+
|
|
1127
|
+
// Create virtual environment if it doesn't exist
|
|
1128
|
+
if (!fs.existsSync(venvPath)) {
|
|
1129
|
+
console.log(
|
|
1130
|
+
chalk.cyan("\nš¦ Creating Python virtual environment...")
|
|
1131
|
+
);
|
|
1132
|
+
try {
|
|
1133
|
+
execSync(`python3 -m venv .venv`, {
|
|
1134
|
+
cwd: directory,
|
|
1135
|
+
stdio: "inherit",
|
|
1136
|
+
});
|
|
1137
|
+
console.log(
|
|
1138
|
+
chalk.green(" ā Virtual environment created")
|
|
1139
|
+
);
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
console.error(
|
|
1142
|
+
chalk.red("\nā Failed to create virtual environment")
|
|
1143
|
+
);
|
|
1144
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Install dependencies in the virtual environment
|
|
1150
|
+
const depsToInstall = REQUIRED_DEPENDENCIES.python;
|
|
1151
|
+
console.log(
|
|
1152
|
+
chalk.cyan(
|
|
1153
|
+
`\nš¦ Installing Python packages: ${depsToInstall.join(", ")}`
|
|
1154
|
+
)
|
|
1155
|
+
);
|
|
1156
|
+
|
|
1157
|
+
try {
|
|
1158
|
+
execSync(`${venvPip} install ${depsToInstall.join(" ")}`, {
|
|
1159
|
+
cwd: directory,
|
|
1160
|
+
stdio: "inherit",
|
|
1161
|
+
});
|
|
1162
|
+
console.log(chalk.green(" ā Python packages installed"));
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
console.error(
|
|
1165
|
+
chalk.red("\nā Failed to install Python packages")
|
|
1166
|
+
);
|
|
1167
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Step 6: Generate test files (wrapper + additional files like pom.xml for Java)
|
|
1173
|
+
console.log(chalk.cyan("\nš Generating test files..."));
|
|
1174
|
+
|
|
1175
|
+
// Get app name from config or directory name
|
|
1176
|
+
const appName = config?.app || path.basename(directory);
|
|
1177
|
+
|
|
1178
|
+
const testFiles = generateTestFiles(
|
|
1179
|
+
language,
|
|
1180
|
+
handlerFile,
|
|
1181
|
+
handlerFunction,
|
|
1182
|
+
appName
|
|
1183
|
+
);
|
|
1184
|
+
|
|
1185
|
+
if (!testFiles || testFiles.length === 0) {
|
|
1186
|
+
console.error(
|
|
1187
|
+
chalk.red(
|
|
1188
|
+
`\nā Failed to generate test files for language: ${language}`
|
|
1189
|
+
)
|
|
1190
|
+
);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Write all generated files
|
|
1195
|
+
for (const file of testFiles) {
|
|
1196
|
+
const filePath = path.join(directory, file.path);
|
|
1197
|
+
|
|
1198
|
+
// Create directories if needed
|
|
1199
|
+
const fileDir = path.dirname(filePath);
|
|
1200
|
+
if (!fs.existsSync(fileDir)) {
|
|
1201
|
+
fs.mkdirSync(fileDir, { recursive: true });
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
fs.writeFileSync(filePath, file.content, "utf8");
|
|
1205
|
+
console.log(chalk.dim(` Created: ${file.path}`));
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
// Step 7: Determine start command
|
|
1209
|
+
const startCmd = getStartCommand(language, directory, customCommand);
|
|
1210
|
+
|
|
1211
|
+
// Step 8: Set environment variables
|
|
1212
|
+
const env = getTestEnvironmentVariables(port, language);
|
|
1213
|
+
|
|
1214
|
+
// Step 9: Display startup message
|
|
1215
|
+
displayTestStartupMessage(port);
|
|
1216
|
+
|
|
1217
|
+
// Step 10: Start the server
|
|
1218
|
+
childProcess = spawn(startCmd.command, startCmd.args, {
|
|
1219
|
+
cwd: directory,
|
|
1220
|
+
env,
|
|
1221
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
1222
|
+
shell: process.platform === "win32",
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
// Stream stdout
|
|
1226
|
+
childProcess.stdout.on("data", (data) => {
|
|
1227
|
+
process.stdout.write(chalk.white(data.toString()));
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1230
|
+
// Stream stderr
|
|
1231
|
+
childProcess.stderr.on("data", (data) => {
|
|
1232
|
+
process.stderr.write(chalk.red(data.toString()));
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1235
|
+
// Handle process exit
|
|
1236
|
+
childProcess.on("close", (code) => {
|
|
1237
|
+
console.log(
|
|
1238
|
+
chalk.yellow(`\nš Server stopped with exit code: ${code}`)
|
|
1239
|
+
);
|
|
1240
|
+
cleanupGeneratedFiles(directory, language, retain);
|
|
1241
|
+
process.exit(code || 0);
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
// Handle process error
|
|
1245
|
+
childProcess.on("error", (error) => {
|
|
1246
|
+
console.error(
|
|
1247
|
+
chalk.red(`\nā Failed to start server: ${error.message}`)
|
|
1248
|
+
);
|
|
1249
|
+
|
|
1250
|
+
if (error.code === "ENOENT") {
|
|
1251
|
+
console.log(
|
|
1252
|
+
chalk.yellow(
|
|
1253
|
+
`\nš” Hint: Make sure the command "${startCmd.command}" is installed and available in PATH.`
|
|
1254
|
+
)
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
cleanupGeneratedFiles(directory, language, retain);
|
|
1259
|
+
process.exit(1);
|
|
1260
|
+
});
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
if (
|
|
1263
|
+
error.message &&
|
|
1264
|
+
error.message.includes("User force closed the prompt")
|
|
1265
|
+
) {
|
|
1266
|
+
console.log(chalk.yellow("\nā ļø Operation cancelled by user"));
|
|
1267
|
+
if (language && directory) {
|
|
1268
|
+
cleanupGeneratedFiles(directory, language, retain);
|
|
1269
|
+
}
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
console.error(
|
|
1274
|
+
chalk.red("\nā An error occurred:"),
|
|
1275
|
+
error.message || "Unknown error"
|
|
1276
|
+
);
|
|
1277
|
+
|
|
1278
|
+
if (language && directory) {
|
|
1279
|
+
cleanupGeneratedFiles(directory, language, retain);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Handle container type serverless test - runs docker container locally
|
|
1286
|
+
*/
|
|
1287
|
+
async function handleContainerTest(config, directory, port) {
|
|
1288
|
+
const containerOpts = config.serverlessConfig?.ContainerOpts;
|
|
1289
|
+
const image = containerOpts?.Image;
|
|
1290
|
+
|
|
1291
|
+
if (!image) {
|
|
1292
|
+
console.error(
|
|
1293
|
+
chalk.red(
|
|
1294
|
+
"\nā Container image not found in boltic-properties.yaml"
|
|
1295
|
+
)
|
|
1296
|
+
);
|
|
1297
|
+
console.log(
|
|
1298
|
+
chalk.yellow(
|
|
1299
|
+
"Please ensure ContainerOpts.Image is set in serverlessConfig."
|
|
1300
|
+
)
|
|
1301
|
+
);
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
console.log(chalk.cyan("\nš³ Container serverless detected"));
|
|
1306
|
+
console.log(chalk.dim(` Image: ${image}`));
|
|
1307
|
+
console.log(chalk.dim(` Port: ${port}`));
|
|
1308
|
+
|
|
1309
|
+
// Check if Docker is available
|
|
1310
|
+
try {
|
|
1311
|
+
execSync("docker --version", { stdio: "pipe" });
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
console.error(
|
|
1314
|
+
chalk.red("\nā Docker is not installed or not available in PATH.")
|
|
1315
|
+
);
|
|
1316
|
+
console.log(
|
|
1317
|
+
chalk.yellow(
|
|
1318
|
+
"Please install Docker to test container type serverless."
|
|
1319
|
+
)
|
|
1320
|
+
);
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// Build environment variables from config
|
|
1325
|
+
const envVars = config.serverlessConfig?.Env || {};
|
|
1326
|
+
const envArgs = Object.entries(envVars).flatMap(([key, value]) => [
|
|
1327
|
+
"-e",
|
|
1328
|
+
`${key}=${value}`,
|
|
1329
|
+
]);
|
|
1330
|
+
|
|
1331
|
+
// Build docker run command
|
|
1332
|
+
const dockerArgs = ["run", "--rm", "-p", `${port}:8080`, ...envArgs, image];
|
|
1333
|
+
|
|
1334
|
+
console.log("\n" + chalk.bgCyan.black(" š§Ŗ LOCAL CONTAINER TEST ") + "\n");
|
|
1335
|
+
console.log(
|
|
1336
|
+
chalk.green(`š Starting container on http://localhost:${port}`)
|
|
1337
|
+
);
|
|
1338
|
+
console.log();
|
|
1339
|
+
console.log(chalk.dim("ā".repeat(60)));
|
|
1340
|
+
console.log(chalk.dim(" Press Ctrl+C to stop the container"));
|
|
1341
|
+
console.log(chalk.dim("ā".repeat(60)));
|
|
1342
|
+
console.log();
|
|
1343
|
+
|
|
1344
|
+
// Start the container
|
|
1345
|
+
const dockerProcess = spawn("docker", dockerArgs, {
|
|
1346
|
+
cwd: directory,
|
|
1347
|
+
stdio: ["inherit", "pipe", "pipe"],
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
// Stream stdout
|
|
1351
|
+
dockerProcess.stdout.on("data", (data) => {
|
|
1352
|
+
process.stdout.write(chalk.white(data.toString()));
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
// Stream stderr
|
|
1356
|
+
dockerProcess.stderr.on("data", (data) => {
|
|
1357
|
+
process.stderr.write(chalk.yellow(data.toString()));
|
|
1358
|
+
});
|
|
1359
|
+
|
|
1360
|
+
// Handle process exit
|
|
1361
|
+
dockerProcess.on("close", (code) => {
|
|
1362
|
+
console.log(
|
|
1363
|
+
chalk.yellow(`\nš Container stopped with exit code: ${code}`)
|
|
1364
|
+
);
|
|
1365
|
+
process.exit(code || 0);
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
// Handle process error
|
|
1369
|
+
dockerProcess.on("error", (error) => {
|
|
1370
|
+
console.error(
|
|
1371
|
+
chalk.red(`\nā Failed to start container: ${error.message}`)
|
|
1372
|
+
);
|
|
1373
|
+
if (error.code === "ENOENT") {
|
|
1374
|
+
console.log(
|
|
1375
|
+
chalk.yellow(
|
|
1376
|
+
"\nš” Hint: Make sure Docker is installed and available in PATH."
|
|
1377
|
+
)
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
process.exit(1);
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
// Handle Ctrl+C
|
|
1384
|
+
const cleanup = (signal) => {
|
|
1385
|
+
console.log(
|
|
1386
|
+
chalk.yellow(`\n\nš Received ${signal}, stopping container...`)
|
|
1387
|
+
);
|
|
1388
|
+
dockerProcess.kill("SIGTERM");
|
|
1389
|
+
};
|
|
1390
|
+
|
|
1391
|
+
process.on("SIGINT", () => cleanup("SIGINT"));
|
|
1392
|
+
process.on("SIGTERM", () => cleanup("SIGTERM"));
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
async function handlePull(args) {
|
|
1396
|
+
console.log(chalk.green("Pulling serverless..."));
|
|
1397
|
+
try {
|
|
1398
|
+
// Parse command line arguments
|
|
1399
|
+
let currentDir = process.cwd();
|
|
1400
|
+
const pathIndex = args.indexOf("--path");
|
|
1401
|
+
|
|
1402
|
+
if (pathIndex !== -1 && args[pathIndex + 1]) {
|
|
1403
|
+
currentDir = args[pathIndex + 1];
|
|
1404
|
+
// Validate the provided path
|
|
1405
|
+
if (!fs.existsSync(currentDir)) {
|
|
1406
|
+
console.error(
|
|
1407
|
+
chalk.red(
|
|
1408
|
+
`Error: The specified path does not exist: ${currentDir}`
|
|
1409
|
+
)
|
|
1410
|
+
);
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
const { apiUrl, token, accountId, session } = await getCurrentEnv();
|
|
1415
|
+
|
|
1416
|
+
console.log(
|
|
1417
|
+
chalk.green(
|
|
1418
|
+
"Please select the serverless to pull from the list below:"
|
|
1419
|
+
)
|
|
1420
|
+
);
|
|
1421
|
+
|
|
1422
|
+
const allServerless = await listAllServerless(
|
|
1423
|
+
apiUrl,
|
|
1424
|
+
token,
|
|
1425
|
+
accountId,
|
|
1426
|
+
session
|
|
1427
|
+
);
|
|
1428
|
+
if (!allServerless || !Array.isArray(allServerless)) {
|
|
1429
|
+
console.error(
|
|
1430
|
+
chalk.red(
|
|
1431
|
+
"\nā Failed to fetch serverless: Invalid response format"
|
|
1432
|
+
)
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1435
|
+
if (allServerless.length === 0) {
|
|
1436
|
+
console.error(chalk.red("\nā No serverless found."));
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
// Let user select an integration
|
|
1440
|
+
const choices =
|
|
1441
|
+
allServerless.map((serverless) => {
|
|
1442
|
+
const runtime = serverless.Config?.Runtime || "code";
|
|
1443
|
+
const typeIcon =
|
|
1444
|
+
runtime === "git"
|
|
1445
|
+
? "š¦"
|
|
1446
|
+
: runtime === "container"
|
|
1447
|
+
? "š³"
|
|
1448
|
+
: "š";
|
|
1449
|
+
const language = serverless.Config?.CodeOpts?.Language;
|
|
1450
|
+
return {
|
|
1451
|
+
name: `${serverless.Config.Name}: ${typeIcon} ${runtime} | Status - ${serverless.Status}${language ? ` | language: ${language}` : ""}`,
|
|
1452
|
+
value: serverless,
|
|
1453
|
+
};
|
|
1454
|
+
}) || [];
|
|
1455
|
+
|
|
1456
|
+
const selectedServerless = await search({
|
|
1457
|
+
message: "Search and select an serverless to edit:",
|
|
1458
|
+
source: async (term) => {
|
|
1459
|
+
if (!term) return choices;
|
|
1460
|
+
return choices?.filter((choice) =>
|
|
1461
|
+
choice.name.toLowerCase().includes(term.toLowerCase())
|
|
1462
|
+
);
|
|
1463
|
+
},
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1466
|
+
console.log(
|
|
1467
|
+
chalk.cyan("\nSelected serverless:"),
|
|
1468
|
+
selectedServerless.Config.Name
|
|
1469
|
+
);
|
|
1470
|
+
const pulledServerless = await pullServerless(
|
|
1471
|
+
apiUrl,
|
|
1472
|
+
token,
|
|
1473
|
+
accountId,
|
|
1474
|
+
session,
|
|
1475
|
+
selectedServerless.ID
|
|
1476
|
+
);
|
|
1477
|
+
if (!pulledServerless) {
|
|
1478
|
+
console.error(
|
|
1479
|
+
chalk.red(
|
|
1480
|
+
"\nā Failed to fetch serverless details. Please try again later."
|
|
1481
|
+
)
|
|
1482
|
+
);
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
// console.log("selectes serverless : ",pulledServerless)
|
|
1486
|
+
|
|
1487
|
+
// Get the app name, language and type for the folder name
|
|
1488
|
+
const appName =
|
|
1489
|
+
pulledServerless?.Config?.Name || selectedServerless.Config?.Name;
|
|
1490
|
+
const language =
|
|
1491
|
+
pulledServerless?.Config?.CodeOpts?.Language?.split("/")[0] ||
|
|
1492
|
+
"nodejs";
|
|
1493
|
+
const serverlessType = pulledServerless?.Config?.Runtime || "code";
|
|
1494
|
+
|
|
1495
|
+
// Create folder name similar to create command
|
|
1496
|
+
const folderName = appName;
|
|
1497
|
+
const targetDir = path.join(currentDir, folderName);
|
|
1498
|
+
|
|
1499
|
+
// Check if folder already exists
|
|
1500
|
+
if (fs.existsSync(targetDir)) {
|
|
1501
|
+
console.error(
|
|
1502
|
+
chalk.red(
|
|
1503
|
+
`\nā Folder "${folderName}" already exists in ${currentDir}. Please remove it or use a different location.`
|
|
1504
|
+
)
|
|
1505
|
+
);
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// Create the folder
|
|
1510
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
1511
|
+
console.log(chalk.cyan(`\nš Creating folder: ${folderName}`));
|
|
1512
|
+
|
|
1513
|
+
// Create the files (boltic-properties.yaml with serverlessId and serverlessConfig, handler file with code)
|
|
1514
|
+
try {
|
|
1515
|
+
const result = createPulledServerlessFiles(
|
|
1516
|
+
targetDir,
|
|
1517
|
+
pulledServerless,
|
|
1518
|
+
serverlessType
|
|
1519
|
+
);
|
|
1520
|
+
|
|
1521
|
+
// If there was an error (e.g., no SSH access for git type), don't show success
|
|
1522
|
+
if (result?.error) {
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
displayPullSuccessMessage(appName, targetDir);
|
|
1527
|
+
} catch (fileError) {
|
|
1528
|
+
console.error(
|
|
1529
|
+
chalk.red("\nā Failed to create files:"),
|
|
1530
|
+
fileError.message
|
|
1531
|
+
);
|
|
1532
|
+
// Clean up the created folder on error
|
|
1533
|
+
try {
|
|
1534
|
+
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
1535
|
+
} catch (cleanupError) {
|
|
1536
|
+
// Ignore cleanup errors
|
|
1537
|
+
}
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1540
|
+
} catch (error) {
|
|
1541
|
+
if (
|
|
1542
|
+
error.message &&
|
|
1543
|
+
error.message.includes("User force closed the prompt")
|
|
1544
|
+
) {
|
|
1545
|
+
console.log(chalk.yellow("\nā ļø Operation cancelled by user"));
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
// Handle other errors
|
|
1549
|
+
console.error(
|
|
1550
|
+
chalk.red("\nā An error occurred:"),
|
|
1551
|
+
error.message || "Unknown error"
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
function showHelp() {
|
|
1557
|
+
console.log(chalk.cyan("\nServerless Commands:\n"));
|
|
1558
|
+
Object.entries(commands).forEach(([cmd, details]) => {
|
|
1559
|
+
console.log(chalk.bold(` ${cmd}`) + ` - ${details.description}`);
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
console.log(chalk.cyan("\nCreate Command Options:\n"));
|
|
1563
|
+
console.log(
|
|
1564
|
+
chalk.bold(" --type, -t") +
|
|
1565
|
+
chalk.dim(" ") +
|
|
1566
|
+
"Serverless type: blueprint, git, or container (prompts if not provided)"
|
|
1567
|
+
);
|
|
1568
|
+
console.log(
|
|
1569
|
+
chalk.bold(" --name, -n") +
|
|
1570
|
+
chalk.dim(" ") +
|
|
1571
|
+
"Name of the serverless function (required, prompts if not provided)"
|
|
1572
|
+
);
|
|
1573
|
+
console.log(
|
|
1574
|
+
chalk.bold(" --language, -l") +
|
|
1575
|
+
chalk.dim(" ") +
|
|
1576
|
+
"Programming language: nodejs, python, golang, java (prompts if not provided)"
|
|
1577
|
+
);
|
|
1578
|
+
console.log(
|
|
1579
|
+
chalk.bold(" --directory, -d") +
|
|
1580
|
+
chalk.dim(" ") +
|
|
1581
|
+
"Directory where to create the project (default: current directory)"
|
|
1582
|
+
);
|
|
1583
|
+
|
|
1584
|
+
console.log(chalk.cyan("\nTest Command Options:\n"));
|
|
1585
|
+
console.log(
|
|
1586
|
+
chalk.bold(" --port, -p") +
|
|
1587
|
+
chalk.dim(" ") +
|
|
1588
|
+
"Port to run the server on (default: 8080)"
|
|
1589
|
+
);
|
|
1590
|
+
console.log(
|
|
1591
|
+
chalk.bold(" --language, -l") +
|
|
1592
|
+
chalk.dim(" ") +
|
|
1593
|
+
"Language (nodejs, python, golang, java) - auto-detected if not specified"
|
|
1594
|
+
);
|
|
1595
|
+
console.log(
|
|
1596
|
+
chalk.bold(" --directory, -d") +
|
|
1597
|
+
chalk.dim(" ") +
|
|
1598
|
+
"Base directory of the project (default: current directory)"
|
|
1599
|
+
);
|
|
1600
|
+
|
|
1601
|
+
console.log(chalk.cyan("\nPublish Command Options:\n"));
|
|
1602
|
+
console.log(
|
|
1603
|
+
chalk.bold(" --directory, -d") +
|
|
1604
|
+
chalk.dim(" ") +
|
|
1605
|
+
"Directory of the serverless project (default: current directory)"
|
|
1606
|
+
);
|
|
1607
|
+
|
|
1608
|
+
console.log(chalk.cyan("\nStatus Command Options:\n"));
|
|
1609
|
+
console.log(
|
|
1610
|
+
chalk.bold(" --name, -n") +
|
|
1611
|
+
chalk.dim(" ") +
|
|
1612
|
+
"Name of the serverless function (prompts if not provided)"
|
|
1613
|
+
);
|
|
1614
|
+
|
|
1615
|
+
console.log(chalk.cyan("\nCreate Examples:\n"));
|
|
1616
|
+
console.log(
|
|
1617
|
+
chalk.dim(
|
|
1618
|
+
" # Interactive mode (will prompt for type, name, and language)"
|
|
1619
|
+
)
|
|
1620
|
+
);
|
|
1621
|
+
console.log(" boltic serverless create\n");
|
|
1622
|
+
console.log(chalk.dim(" # Create blueprint serverless"));
|
|
1623
|
+
console.log(
|
|
1624
|
+
" boltic serverless create --type blueprint --name my-api --language nodejs\n"
|
|
1625
|
+
);
|
|
1626
|
+
console.log(
|
|
1627
|
+
chalk.dim(
|
|
1628
|
+
" # Create git-based serverless (add your code, then publish)"
|
|
1629
|
+
)
|
|
1630
|
+
);
|
|
1631
|
+
console.log(
|
|
1632
|
+
" boltic serverless create --type git --name my-git-func --language python\n"
|
|
1633
|
+
);
|
|
1634
|
+
console.log(chalk.dim(" # Create container-based serverless"));
|
|
1635
|
+
console.log(
|
|
1636
|
+
" boltic serverless create --type container --name my-container --language golang\n"
|
|
1637
|
+
);
|
|
1638
|
+
console.log(chalk.dim(" # With custom directory"));
|
|
1639
|
+
console.log(
|
|
1640
|
+
" boltic serverless create --type blueprint --name my-function --language python --directory ./projects\n"
|
|
1641
|
+
);
|
|
1642
|
+
|
|
1643
|
+
console.log(chalk.cyan("\nTest Examples:\n"));
|
|
1644
|
+
console.log(chalk.dim(" # Basic usage - auto-detect everything"));
|
|
1645
|
+
console.log(" boltic serverless test\n");
|
|
1646
|
+
console.log(chalk.dim(" # Specify port"));
|
|
1647
|
+
console.log(" boltic serverless test --port 3000\n");
|
|
1648
|
+
|
|
1649
|
+
console.log(chalk.cyan("\nPublish Examples:\n"));
|
|
1650
|
+
console.log(chalk.dim(" # Publish from current directory"));
|
|
1651
|
+
console.log(" boltic serverless publish\n");
|
|
1652
|
+
console.log(chalk.dim(" # Publish from specific directory"));
|
|
1653
|
+
console.log(" boltic serverless publish -d ./my-function\n");
|
|
1654
|
+
|
|
1655
|
+
console.log(chalk.cyan("\nList Examples:\n"));
|
|
1656
|
+
console.log(chalk.dim(" # List all serverless functions"));
|
|
1657
|
+
console.log(" boltic serverless list\n");
|
|
1658
|
+
|
|
1659
|
+
console.log(chalk.cyan("\nStatus Examples:\n"));
|
|
1660
|
+
console.log(chalk.dim(" # Get status by name"));
|
|
1661
|
+
console.log(" boltic serverless status -n my-function\n");
|
|
1662
|
+
console.log(chalk.dim(" # Interactive mode (will prompt for name)"));
|
|
1663
|
+
console.log(" boltic serverless status\n");
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// Execute the serverless command
|
|
1667
|
+
const execute = async (args) => {
|
|
1668
|
+
const subCommand = args[0];
|
|
1669
|
+
|
|
1670
|
+
if (!subCommand) {
|
|
1671
|
+
showHelp();
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
if (!commands[subCommand]) {
|
|
1676
|
+
console.log(chalk.red("Unknown or missing serverless sub-command.\n"));
|
|
1677
|
+
showHelp();
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
const commandObj = commands[subCommand];
|
|
1682
|
+
await commandObj.action(args.slice(1));
|
|
1683
|
+
};
|
|
1684
|
+
|
|
1685
|
+
async function handleList(args = []) {
|
|
1686
|
+
try {
|
|
1687
|
+
const { apiUrl, token, accountId, session } = await getCurrentEnv();
|
|
1688
|
+
|
|
1689
|
+
console.log(chalk.cyan("\nš Fetching serverless functions...\n"));
|
|
1690
|
+
|
|
1691
|
+
const allServerless = await listAllServerless(
|
|
1692
|
+
apiUrl,
|
|
1693
|
+
token,
|
|
1694
|
+
accountId,
|
|
1695
|
+
session
|
|
1696
|
+
);
|
|
1697
|
+
|
|
1698
|
+
if (!allServerless || !Array.isArray(allServerless)) {
|
|
1699
|
+
console.error(
|
|
1700
|
+
chalk.red(
|
|
1701
|
+
"\nā Failed to fetch serverless: Invalid response format"
|
|
1702
|
+
)
|
|
1703
|
+
);
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
if (allServerless.length === 0) {
|
|
1708
|
+
console.log(chalk.yellow("No serverless functions found."));
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
console.log(
|
|
1713
|
+
chalk.green(`Found ${allServerless.length} serverless function(s):`)
|
|
1714
|
+
);
|
|
1715
|
+
console.log(
|
|
1716
|
+
chalk.dim("Use āā to scroll, type to search, Ctrl+C to exit\n")
|
|
1717
|
+
);
|
|
1718
|
+
|
|
1719
|
+
// Build choices for the list
|
|
1720
|
+
const choices = allServerless.map((serverless) => {
|
|
1721
|
+
const runtime = serverless.Config?.Runtime || "code";
|
|
1722
|
+
const typeIcon =
|
|
1723
|
+
runtime === "git"
|
|
1724
|
+
? "š¦"
|
|
1725
|
+
: runtime === "container"
|
|
1726
|
+
? "š³"
|
|
1727
|
+
: "š";
|
|
1728
|
+
const language = serverless.Config?.CodeOpts?.Language;
|
|
1729
|
+
const status = serverless.Status;
|
|
1730
|
+
|
|
1731
|
+
return {
|
|
1732
|
+
name: `${serverless.Config.Name}: ${typeIcon} ${runtime} | Status - ${status}${language ? ` | ${language}` : ""} | ID: ${serverless.ID.substring(0, 8)}...`,
|
|
1733
|
+
value: serverless,
|
|
1734
|
+
};
|
|
1735
|
+
});
|
|
1736
|
+
|
|
1737
|
+
// Show interactive scrollable list
|
|
1738
|
+
const selected = await search({
|
|
1739
|
+
message: "Serverless functions (scroll to browse):",
|
|
1740
|
+
source: async (term) => {
|
|
1741
|
+
if (!term) return choices;
|
|
1742
|
+
return choices.filter((choice) =>
|
|
1743
|
+
choice.name.toLowerCase().includes(term.toLowerCase())
|
|
1744
|
+
);
|
|
1745
|
+
},
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
// Show details of selected serverless
|
|
1749
|
+
if (selected) {
|
|
1750
|
+
const runtime = selected.Config?.Runtime || "code";
|
|
1751
|
+
const typeIcon =
|
|
1752
|
+
runtime === "git"
|
|
1753
|
+
? "š¦"
|
|
1754
|
+
: runtime === "container"
|
|
1755
|
+
? "š³"
|
|
1756
|
+
: "š";
|
|
1757
|
+
|
|
1758
|
+
console.log("\n" + chalk.cyan("ā".repeat(60)));
|
|
1759
|
+
console.log(chalk.bold("\nš Selected Serverless Details:\n"));
|
|
1760
|
+
console.log(
|
|
1761
|
+
chalk.cyan(" Name: ") + chalk.white(selected.Config.Name)
|
|
1762
|
+
);
|
|
1763
|
+
console.log(chalk.cyan(" ID: ") + chalk.white(selected.ID));
|
|
1764
|
+
console.log(
|
|
1765
|
+
chalk.cyan(" Type: ") + chalk.white(`${typeIcon} ${runtime}`)
|
|
1766
|
+
);
|
|
1767
|
+
console.log(
|
|
1768
|
+
chalk.cyan(" Status: ") + chalk.white(selected.Status)
|
|
1769
|
+
);
|
|
1770
|
+
if (selected.Config?.CodeOpts?.Language) {
|
|
1771
|
+
console.log(
|
|
1772
|
+
chalk.cyan(" Language: ") +
|
|
1773
|
+
chalk.white(selected.Config.CodeOpts.Language)
|
|
1774
|
+
);
|
|
1775
|
+
}
|
|
1776
|
+
if (selected.Config?.ContainerOpts?.Image) {
|
|
1777
|
+
console.log(
|
|
1778
|
+
chalk.cyan(" Image: ") +
|
|
1779
|
+
chalk.white(selected.Config.ContainerOpts.Image)
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
console.log(chalk.cyan("ā".repeat(60)));
|
|
1783
|
+
console.log(
|
|
1784
|
+
chalk.dim(
|
|
1785
|
+
"\nUse 'boltic serverless pull' to pull this serverless locally."
|
|
1786
|
+
)
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
if (
|
|
1791
|
+
error.message &&
|
|
1792
|
+
error.message.includes("User force closed the prompt")
|
|
1793
|
+
) {
|
|
1794
|
+
console.log(chalk.yellow("\nā ļø List closed"));
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
console.error(
|
|
1798
|
+
chalk.red("\nā An error occurred:"),
|
|
1799
|
+
error.message || "Unknown error"
|
|
1800
|
+
);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* Handle the status command - show status of a serverless function
|
|
1806
|
+
*/
|
|
1807
|
+
async function handleStatus(args = []) {
|
|
1808
|
+
try {
|
|
1809
|
+
// Parse name from args
|
|
1810
|
+
let name = null;
|
|
1811
|
+
const nameIndex = args.indexOf("--name");
|
|
1812
|
+
const shortNameIndex = args.indexOf("-n");
|
|
1813
|
+
|
|
1814
|
+
if (nameIndex !== -1 && args[nameIndex + 1]) {
|
|
1815
|
+
name = args[nameIndex + 1];
|
|
1816
|
+
} else if (shortNameIndex !== -1 && args[shortNameIndex + 1]) {
|
|
1817
|
+
name = args[shortNameIndex + 1];
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// If name not provided, prompt for it
|
|
1821
|
+
if (!name) {
|
|
1822
|
+
name = await input({
|
|
1823
|
+
message: "Enter serverless name:",
|
|
1824
|
+
validate: (value) => {
|
|
1825
|
+
if (!value || value.trim() === "") {
|
|
1826
|
+
return "Serverless name is required";
|
|
1827
|
+
}
|
|
1828
|
+
return true;
|
|
1829
|
+
},
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
const { apiUrl, token, accountId, session } = await getCurrentEnv();
|
|
1834
|
+
|
|
1835
|
+
console.log(chalk.cyan(`\nš Fetching status for "${name}"...\n`));
|
|
1836
|
+
|
|
1837
|
+
// Get serverless by name using query parameter
|
|
1838
|
+
const result = await listAllServerless(
|
|
1839
|
+
apiUrl,
|
|
1840
|
+
token,
|
|
1841
|
+
accountId,
|
|
1842
|
+
session,
|
|
1843
|
+
name // Pass name as query parameter
|
|
1844
|
+
);
|
|
1845
|
+
|
|
1846
|
+
if (!result || !Array.isArray(result)) {
|
|
1847
|
+
console.error(
|
|
1848
|
+
chalk.red(
|
|
1849
|
+
"\nā Failed to fetch serverless: Invalid response format"
|
|
1850
|
+
)
|
|
1851
|
+
);
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
// Get first element (name is unique)
|
|
1856
|
+
const serverless = result[0];
|
|
1857
|
+
|
|
1858
|
+
if (!serverless) {
|
|
1859
|
+
console.error(chalk.red(`\nā Serverless "${name}" not found.`));
|
|
1860
|
+
console.log(
|
|
1861
|
+
chalk.yellow(
|
|
1862
|
+
"\nUse 'boltic serverless list' to see all serverless functions."
|
|
1863
|
+
)
|
|
1864
|
+
);
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
// Display status
|
|
1869
|
+
const runtime = serverless.Config?.Runtime || "code";
|
|
1870
|
+
const typeIcon =
|
|
1871
|
+
runtime === "git" ? "š¦" : runtime === "container" ? "š³" : "š";
|
|
1872
|
+
const status = serverless.Status;
|
|
1873
|
+
const statusColor =
|
|
1874
|
+
status === "running"
|
|
1875
|
+
? chalk.green
|
|
1876
|
+
: status === "draft"
|
|
1877
|
+
? chalk.yellow
|
|
1878
|
+
: status === "stopped"
|
|
1879
|
+
? chalk.red
|
|
1880
|
+
: chalk.gray;
|
|
1881
|
+
|
|
1882
|
+
console.log(chalk.cyan("ā".repeat(60)));
|
|
1883
|
+
console.log(chalk.bold("\nš Serverless Status\n"));
|
|
1884
|
+
console.log(
|
|
1885
|
+
chalk.cyan(" Name: ") + chalk.white(serverless.Config.Name)
|
|
1886
|
+
);
|
|
1887
|
+
console.log(chalk.cyan(" ID: ") + chalk.white(serverless.ID));
|
|
1888
|
+
console.log(
|
|
1889
|
+
chalk.cyan(" Type: ") + chalk.white(`${typeIcon} ${runtime}`)
|
|
1890
|
+
);
|
|
1891
|
+
console.log(chalk.cyan(" Status: ") + statusColor(status));
|
|
1892
|
+
|
|
1893
|
+
if (serverless.Config?.CodeOpts?.Language) {
|
|
1894
|
+
console.log(
|
|
1895
|
+
chalk.cyan(" Language: ") +
|
|
1896
|
+
chalk.white(serverless.Config.CodeOpts.Language)
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
if (serverless.Config?.ContainerOpts?.Image) {
|
|
1900
|
+
console.log(
|
|
1901
|
+
chalk.cyan(" Image: ") +
|
|
1902
|
+
chalk.white(serverless.Config.ContainerOpts.Image)
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
if (serverless.Config?.Resources) {
|
|
1906
|
+
console.log(
|
|
1907
|
+
chalk.cyan(" Resources: ") +
|
|
1908
|
+
chalk.white(
|
|
1909
|
+
`CPU: ${serverless.Config.Resources.CPU}, Memory: ${serverless.Config.Resources.MemoryMB}MB`
|
|
1910
|
+
)
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
if (serverless.Config?.Scaling) {
|
|
1914
|
+
console.log(
|
|
1915
|
+
chalk.cyan(" Scaling: ") +
|
|
1916
|
+
chalk.white(
|
|
1917
|
+
`Min: ${serverless.Config.Scaling.Min}, Max: ${serverless.Config.Scaling.Max}`
|
|
1918
|
+
)
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
if (serverless.RegionID) {
|
|
1922
|
+
console.log(
|
|
1923
|
+
chalk.cyan(" Region: ") + chalk.white(serverless.RegionID)
|
|
1924
|
+
);
|
|
1925
|
+
}
|
|
1926
|
+
if (serverless.CreatedAt) {
|
|
1927
|
+
console.log(
|
|
1928
|
+
chalk.cyan(" Created: ") +
|
|
1929
|
+
chalk.white(new Date(serverless.CreatedAt).toLocaleString())
|
|
1930
|
+
);
|
|
1931
|
+
}
|
|
1932
|
+
if (serverless.UpdatedAt) {
|
|
1933
|
+
console.log(
|
|
1934
|
+
chalk.cyan(" Updated: ") +
|
|
1935
|
+
chalk.white(new Date(serverless.UpdatedAt).toLocaleString())
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
console.log();
|
|
1940
|
+
console.log(chalk.cyan("ā".repeat(60)));
|
|
1941
|
+
} catch (error) {
|
|
1942
|
+
if (
|
|
1943
|
+
error.message &&
|
|
1944
|
+
error.message.includes("User force closed the prompt")
|
|
1945
|
+
) {
|
|
1946
|
+
console.log(chalk.yellow("\nā ļø Operation cancelled by user"));
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
console.error(
|
|
1950
|
+
chalk.red("\nā An error occurred:"),
|
|
1951
|
+
error.message || "Unknown error"
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
export default {
|
|
1957
|
+
execute,
|
|
1958
|
+
};
|