@forgehive/forge-cli 0.2.3 → 0.2.5
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/dist/runner.js +2 -2
- package/dist/tasks/conf/info.d.ts +1 -5
- package/dist/tasks/conf/info.js +13 -5
- package/dist/tasks/init.js +1 -1
- package/dist/tasks/task/publish.d.ts +3 -1
- package/dist/tasks/task/publish.js +46 -15
- package/dist/tasks/task/run.d.ts +5 -1
- package/dist/tasks/task/run.js +54 -5
- package/dist/test/tasks/init.test.js +2 -2
- package/package.json +4 -4
- package/src/runner.ts +2 -2
- package/src/tasks/conf/info.ts +13 -5
- package/src/tasks/init.ts +1 -1
- package/src/tasks/task/publish.ts +55 -19
- package/src/tasks/task/run.ts +66 -6
- package/src/test/tasks/init.test.ts +2 -2
package/dist/runner.js
CHANGED
|
@@ -26,7 +26,7 @@ const runner = new runner_1.Runner((data) => {
|
|
|
26
26
|
const { _, ...filteredObj } = data;
|
|
27
27
|
return {
|
|
28
28
|
taskName: String(_[0]),
|
|
29
|
-
action:
|
|
29
|
+
action: _[1] ?? '',
|
|
30
30
|
args: filteredObj
|
|
31
31
|
};
|
|
32
32
|
});
|
|
@@ -53,7 +53,7 @@ runner.setHandler(async (data) => {
|
|
|
53
53
|
const parsedArgs = runner.parseArguments(data);
|
|
54
54
|
const { taskName, action, args } = parsedArgs;
|
|
55
55
|
console.log('========================================');
|
|
56
|
-
console.log(
|
|
56
|
+
console.log(`Running: ${taskName} ${action ? action : ''} ${JSON.stringify(args)}`);
|
|
57
57
|
console.log('========================================');
|
|
58
58
|
const task = runner.getTask(taskName);
|
|
59
59
|
if (!task) {
|
|
@@ -3,11 +3,7 @@ export declare const info: import("@forgehive/task").TaskInstanceType<(argv: {},
|
|
|
3
3
|
loadCurrentProfile: (args: {}) => Promise<Promise<import("../types").Profile>>;
|
|
4
4
|
}>) => Promise<{
|
|
5
5
|
version: any;
|
|
6
|
-
profile: {
|
|
7
|
-
name: string;
|
|
8
|
-
url: string;
|
|
9
|
-
apiKey: string;
|
|
10
|
-
};
|
|
6
|
+
profile: {};
|
|
11
7
|
}>, {
|
|
12
8
|
readFile: (filePath: string) => Promise<string>;
|
|
13
9
|
loadCurrentProfile: (args: {}) => Promise<Promise<import("../types").Profile>>;
|
package/dist/tasks/conf/info.js
CHANGED
|
@@ -51,13 +51,21 @@ exports.info = (0, task_1.createTask)(schema, boundaries, async function (_argv,
|
|
|
51
51
|
const packageJsonPath = path.join(__dirname, '../../../package.json');
|
|
52
52
|
const packageJsonContent = await readFile(packageJsonPath);
|
|
53
53
|
const packageJson = JSON.parse(packageJsonContent);
|
|
54
|
-
const
|
|
55
|
-
return {
|
|
54
|
+
const info = {
|
|
56
55
|
version: packageJson.version,
|
|
57
|
-
profile: {
|
|
56
|
+
profile: {}
|
|
57
|
+
};
|
|
58
|
+
let profile;
|
|
59
|
+
try {
|
|
60
|
+
profile = await loadCurrentProfile({});
|
|
61
|
+
info.profile = {
|
|
58
62
|
name: profile.name,
|
|
59
63
|
url: profile.url,
|
|
60
64
|
apiKey: profile.apiKey
|
|
61
|
-
}
|
|
62
|
-
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.log('No default profile set. Please run forge task:run auth:add to create a profile.');
|
|
69
|
+
}
|
|
70
|
+
return info;
|
|
63
71
|
});
|
package/dist/tasks/init.js
CHANGED
|
@@ -17,10 +17,11 @@ export declare const publish: import("@forgehive/task").TaskInstanceType<(argv:
|
|
|
17
17
|
readFileUtf8: (filePath: string) => Promise<string>;
|
|
18
18
|
readFileBinary: (filePath: string) => Promise<Buffer>;
|
|
19
19
|
publishTask: (data: any, profile: Profile) => Promise<any>;
|
|
20
|
+
uploadBundleWithPresignedUrl: (presignedUrl: string, bundleContent: Buffer) => Promise<any>;
|
|
20
21
|
ensureBuildsFolder: () => Promise<string>;
|
|
21
22
|
}>) => Promise<{
|
|
22
23
|
descriptor: import("../types").TaskDescriptor;
|
|
23
|
-
|
|
24
|
+
publish: boolean;
|
|
24
25
|
}>, {
|
|
25
26
|
getCwd: () => Promise<string>;
|
|
26
27
|
loadConf: (args: {}) => Promise<Promise<import("../types").ForgeConf>>;
|
|
@@ -37,5 +38,6 @@ export declare const publish: import("@forgehive/task").TaskInstanceType<(argv:
|
|
|
37
38
|
readFileUtf8: (filePath: string) => Promise<string>;
|
|
38
39
|
readFileBinary: (filePath: string) => Promise<Buffer>;
|
|
39
40
|
publishTask: (data: any, profile: Profile) => Promise<any>;
|
|
41
|
+
uploadBundleWithPresignedUrl: (presignedUrl: string, bundleContent: Buffer) => Promise<any>;
|
|
40
42
|
ensureBuildsFolder: () => Promise<string>;
|
|
41
43
|
}>;
|
|
@@ -37,15 +37,32 @@ const boundaries = {
|
|
|
37
37
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
38
|
publishTask: async (data, profile) => {
|
|
39
39
|
const publishUrl = `${profile.url}/api/tasks/publish`;
|
|
40
|
-
console.log(`Publishing task to ${publishUrl}...`);
|
|
41
40
|
const authToken = `${profile.apiKey}:${profile.apiSecret}`;
|
|
42
|
-
|
|
41
|
+
try {
|
|
42
|
+
const response = await axios_1.default.post(publishUrl, data, {
|
|
43
|
+
headers: {
|
|
44
|
+
Authorization: `Bearer ${authToken}`,
|
|
45
|
+
'Content-Type': 'application/json'
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
return response.data;
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (error.response?.data?.error.includes('Bundle size')) {
|
|
53
|
+
throw new Error('Bundle size exceeds the maximum allowed size of 25MB');
|
|
54
|
+
}
|
|
55
|
+
throw new Error('Failed to publish task source code and metadata');
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
+
uploadBundleWithPresignedUrl: async (presignedUrl, bundleContent) => {
|
|
60
|
+
const response = await axios_1.default.put(presignedUrl, bundleContent, {
|
|
43
61
|
headers: {
|
|
44
|
-
|
|
45
|
-
'Content-Type': 'application/json'
|
|
62
|
+
'Content-Type': 'application/octet-stream'
|
|
46
63
|
}
|
|
47
64
|
});
|
|
48
|
-
return response.
|
|
65
|
+
return response.status === 200;
|
|
49
66
|
},
|
|
50
67
|
ensureBuildsFolder: async () => {
|
|
51
68
|
const buildsPath = path_1.default.join(os_1.default.homedir(), '.forge', 'builds');
|
|
@@ -58,7 +75,7 @@ const boundaries = {
|
|
|
58
75
|
return buildsPath;
|
|
59
76
|
}
|
|
60
77
|
};
|
|
61
|
-
exports.publish = (0, task_1.createTask)(schema, boundaries, async function ({ descriptorName }, { getCwd, ensureBuildsFolder, loadConf, bundleCreate, bundleLoad, readFileUtf8, readFileBinary, publishTask, loadCurrentProfile }) {
|
|
78
|
+
exports.publish = (0, task_1.createTask)(schema, boundaries, async function ({ descriptorName }, { getCwd, ensureBuildsFolder, loadConf, bundleCreate, bundleLoad, readFileUtf8, readFileBinary, publishTask, loadCurrentProfile, uploadBundleWithPresignedUrl }) {
|
|
62
79
|
const cwd = await getCwd();
|
|
63
80
|
const forgeJson = await loadConf({});
|
|
64
81
|
const profile = await loadCurrentProfile({});
|
|
@@ -70,14 +87,12 @@ exports.publish = (0, task_1.createTask)(schema, boundaries, async function ({ d
|
|
|
70
87
|
const entryPoint = path_1.default.join(cwd, taskDescriptor.path);
|
|
71
88
|
const buildsPath = await ensureBuildsFolder();
|
|
72
89
|
const outputFile = path_1.default.join(buildsPath, `${descriptorName}.js`);
|
|
73
|
-
console.log('entryPoint:', entryPoint);
|
|
74
|
-
console.log('buildsPath:', buildsPath);
|
|
75
|
-
console.log('outputFile:', outputFile);
|
|
76
90
|
// Bundle the task
|
|
77
91
|
await bundleCreate({
|
|
78
92
|
entryPoint,
|
|
79
93
|
outputFile
|
|
80
94
|
});
|
|
95
|
+
console.log('Bundle created...');
|
|
81
96
|
// Load the bundled task
|
|
82
97
|
const bundle = await bundleLoad({
|
|
83
98
|
bundlePath: outputFile
|
|
@@ -86,21 +101,37 @@ exports.publish = (0, task_1.createTask)(schema, boundaries, async function ({ d
|
|
|
86
101
|
const task = bundle[taskDescriptor.handler];
|
|
87
102
|
const description = task.getDescription() ?? '';
|
|
88
103
|
const schema = task.getSchema() || new schema_1.Schema({});
|
|
104
|
+
const boundaries = Object.keys(task.getBoundaries()) || [];
|
|
89
105
|
const schemaDescriptor = schema.describe();
|
|
90
|
-
// Read the task file content
|
|
106
|
+
// Read the task file content
|
|
91
107
|
const sourceCode = await readFileUtf8(entryPoint);
|
|
92
108
|
const bundleContent = await readFileBinary(outputFile);
|
|
109
|
+
// Get bundle size
|
|
110
|
+
const bundleSize = bundleContent.length;
|
|
111
|
+
// First, publish task metadata and get presigned URL for bundle upload
|
|
93
112
|
const data = {
|
|
94
113
|
...taskDescriptor,
|
|
95
114
|
taskName: descriptorName,
|
|
96
115
|
projectName,
|
|
97
116
|
description,
|
|
98
117
|
schemaDescriptor: JSON.stringify(schemaDescriptor),
|
|
118
|
+
boundaries,
|
|
99
119
|
sourceCode,
|
|
100
|
-
|
|
120
|
+
bundleSize
|
|
101
121
|
};
|
|
102
|
-
// Publish to hive api server
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
122
|
+
// Publish metadata to hive api server
|
|
123
|
+
console.log(`Publishing metadata and source code to ${profile.url}...`);
|
|
124
|
+
const publishResponse = await publishTask(data, profile);
|
|
125
|
+
// Upload bundle using the presigned URL
|
|
126
|
+
if (publishResponse.bundleUploadUrl) {
|
|
127
|
+
console.log('Uploading bundle...');
|
|
128
|
+
await uploadBundleWithPresignedUrl(publishResponse.bundleUploadUrl, bundleContent);
|
|
129
|
+
return {
|
|
130
|
+
descriptor: taskDescriptor,
|
|
131
|
+
publish: true
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
throw new Error('Bundle upload failed');
|
|
136
|
+
}
|
|
106
137
|
});
|
package/dist/tasks/task/run.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { type ForgeConf } from '../types';
|
|
1
|
+
import { type ForgeConf, type Profile } from '../types';
|
|
2
2
|
export declare const run: import("@forgehive/task").TaskInstanceType<(argv: {
|
|
3
3
|
descriptorName: string;
|
|
4
4
|
args: Record<string, string | number | boolean>;
|
|
5
5
|
}, boundaries: import("@forgehive/task").WrappedBoundaries<{
|
|
6
6
|
loadConf: (args: {}) => Promise<Promise<ForgeConf>>;
|
|
7
|
+
loadCurrentProfile: (args: {}) => Promise<Promise<Profile>>;
|
|
7
8
|
bundleCreate: (args: {
|
|
8
9
|
entryPoint: string;
|
|
9
10
|
outputFile: string;
|
|
@@ -15,8 +16,10 @@ export declare const run: import("@forgehive/task").TaskInstanceType<(argv: {
|
|
|
15
16
|
}) => Promise<Promise<any>>;
|
|
16
17
|
verifyLogFolder: (logsPath: string) => Promise<boolean>;
|
|
17
18
|
ensureBuildsFolder: () => Promise<string>;
|
|
19
|
+
sendLogToAPI: (profile: Profile, projectName: string, taskName: string, logItem: unknown) => Promise<boolean>;
|
|
18
20
|
}>) => Promise<any>, {
|
|
19
21
|
loadConf: (args: {}) => Promise<Promise<ForgeConf>>;
|
|
22
|
+
loadCurrentProfile: (args: {}) => Promise<Promise<Profile>>;
|
|
20
23
|
bundleCreate: (args: {
|
|
21
24
|
entryPoint: string;
|
|
22
25
|
outputFile: string;
|
|
@@ -28,4 +31,5 @@ export declare const run: import("@forgehive/task").TaskInstanceType<(argv: {
|
|
|
28
31
|
}) => Promise<Promise<any>>;
|
|
29
32
|
verifyLogFolder: (logsPath: string) => Promise<boolean>;
|
|
30
33
|
ensureBuildsFolder: () => Promise<string>;
|
|
34
|
+
sendLogToAPI: (profile: Profile, projectName: string, taskName: string, logItem: unknown) => Promise<boolean>;
|
|
31
35
|
}>;
|
package/dist/tasks/task/run.js
CHANGED
|
@@ -10,12 +10,14 @@ exports.run = void 0;
|
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
12
12
|
const os_1 = __importDefault(require("os"));
|
|
13
|
+
const axios_1 = __importDefault(require("axios"));
|
|
13
14
|
const task_1 = require("@forgehive/task");
|
|
14
15
|
const schema_1 = require("@forgehive/schema");
|
|
15
16
|
const record_tape_1 = require("@forgehive/record-tape");
|
|
16
17
|
const create_1 = require("../bundle/create");
|
|
17
18
|
const load_1 = require("../bundle/load");
|
|
18
19
|
const load_2 = require("../conf/load");
|
|
20
|
+
const loadCurrent_1 = require("../auth/loadCurrent");
|
|
19
21
|
// For now, we'll use a simple schema without the record type
|
|
20
22
|
// TODO: Use Schema.record once it's properly built and available
|
|
21
23
|
const schema = new schema_1.Schema({
|
|
@@ -25,6 +27,7 @@ const schema = new schema_1.Schema({
|
|
|
25
27
|
});
|
|
26
28
|
const boundaries = {
|
|
27
29
|
loadConf: load_2.load.asBoundary(),
|
|
30
|
+
loadCurrentProfile: loadCurrent_1.loadCurrent.asBoundary(),
|
|
28
31
|
bundleCreate: create_1.create.asBoundary(),
|
|
29
32
|
bundleLoad: load_1.load.asBoundary(),
|
|
30
33
|
verifyLogFolder: async (logsPath) => {
|
|
@@ -46,15 +49,49 @@ const boundaries = {
|
|
|
46
49
|
await promises_1.default.mkdir(buildsPath, { recursive: true });
|
|
47
50
|
}
|
|
48
51
|
return buildsPath;
|
|
52
|
+
},
|
|
53
|
+
sendLogToAPI: async (profile, projectName, taskName, logItem) => {
|
|
54
|
+
try {
|
|
55
|
+
const logsUrl = `${profile.url}/api/tasks/log-ingest`;
|
|
56
|
+
const authToken = `${profile.apiKey}:${profile.apiSecret}`;
|
|
57
|
+
await axios_1.default.post(logsUrl, {
|
|
58
|
+
projectName,
|
|
59
|
+
taskName,
|
|
60
|
+
logItem: JSON.stringify(logItem)
|
|
61
|
+
}, {
|
|
62
|
+
headers: {
|
|
63
|
+
Authorization: `Bearer ${authToken}`,
|
|
64
|
+
'Content-Type': 'application/json'
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
console.log('===============================================');
|
|
68
|
+
console.log('Log sent to API... ', profile.name, profile.url);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
const error = e;
|
|
73
|
+
console.error('Failed to send log to API:', error.message);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
49
76
|
}
|
|
50
77
|
};
|
|
51
|
-
exports.run = (0, task_1.createTask)(schema, boundaries, async function ({ descriptorName, args }, { loadConf, bundleCreate, bundleLoad, verifyLogFolder, ensureBuildsFolder }) {
|
|
78
|
+
exports.run = (0, task_1.createTask)(schema, boundaries, async function ({ descriptorName, args }, { loadConf, bundleCreate, bundleLoad, verifyLogFolder, ensureBuildsFolder, loadCurrentProfile, sendLogToAPI }) {
|
|
52
79
|
// Load forge configuration
|
|
53
80
|
const forge = await loadConf({});
|
|
54
81
|
const taskDescriptor = forge.tasks[descriptorName];
|
|
82
|
+
const projectName = forge.project.name;
|
|
55
83
|
if (taskDescriptor === undefined) {
|
|
56
84
|
throw new Error('Task is not defined on forge.json');
|
|
57
85
|
}
|
|
86
|
+
// Try to load profile, but continue if not found
|
|
87
|
+
let profile = null;
|
|
88
|
+
try {
|
|
89
|
+
profile = await loadCurrentProfile({});
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
// Profile not found or not configured, continue without it
|
|
93
|
+
console.log('No profile found, logs will not be sent to remote API');
|
|
94
|
+
}
|
|
58
95
|
// Verify if log folder exists
|
|
59
96
|
const logFolderPath = path_1.default.join(process.cwd(), forge.paths.logs);
|
|
60
97
|
const logFolderExists = await verifyLogFolder(logFolderPath);
|
|
@@ -105,14 +142,26 @@ exports.run = (0, task_1.createTask)(schema, boundaries, async function ({ descr
|
|
|
105
142
|
}
|
|
106
143
|
tape.recordFrom(descriptorName, task);
|
|
107
144
|
// Run the task with provided arguments
|
|
108
|
-
let result;
|
|
145
|
+
let result, error;
|
|
109
146
|
try {
|
|
110
147
|
result = await task.run(args);
|
|
111
148
|
}
|
|
112
|
-
catch (
|
|
113
|
-
|
|
114
|
-
throw error;
|
|
149
|
+
catch (e) {
|
|
150
|
+
error = e;
|
|
115
151
|
}
|
|
116
152
|
await tape.save();
|
|
153
|
+
const logItems = tape.getLog();
|
|
154
|
+
const lastLogItem = logItems[logItems.length - 1];
|
|
155
|
+
if (profile) {
|
|
156
|
+
try {
|
|
157
|
+
await sendLogToAPI(profile, projectName, descriptorName, lastLogItem);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
console.error('Failed to send log to API:', e);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (error) {
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
117
166
|
return result;
|
|
118
167
|
});
|
|
@@ -43,7 +43,7 @@ describe('Init task', () => {
|
|
|
43
43
|
const fileContent = await fs.promises.readFile(path_1.default.join(rootDir, 'forge.json'), 'utf-8');
|
|
44
44
|
const config = JSON.parse(fileContent);
|
|
45
45
|
// Verify the file content
|
|
46
|
-
expect(config).toHaveProperty('project.name', '
|
|
46
|
+
expect(config).toHaveProperty('project.name', 'BaseProject');
|
|
47
47
|
expect(config).toHaveProperty('paths.logs', 'logs/');
|
|
48
48
|
expect(config).toHaveProperty('paths.tasks', 'src/tasks/');
|
|
49
49
|
expect(config).toHaveProperty('infra.region', 'us-west-2');
|
|
@@ -66,7 +66,7 @@ describe('Init task', () => {
|
|
|
66
66
|
// Verify saveFile was not called
|
|
67
67
|
expect(saveFileFn).not.toHaveBeenCalled();
|
|
68
68
|
// Verify the returned config has the correct structure
|
|
69
|
-
expect(result).toHaveProperty('project.name', '
|
|
69
|
+
expect(result).toHaveProperty('project.name', 'BaseProject');
|
|
70
70
|
expect(result).toHaveProperty('paths.logs', 'logs/');
|
|
71
71
|
expect(result).toHaveProperty('paths.tasks', 'src/tasks/');
|
|
72
72
|
expect(result).toHaveProperty('infra.region', 'us-west-2');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgehive/forge-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "TypeScript CLI application",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"esbuild": "^0.25.0",
|
|
26
26
|
"handlebars": "^4.7.8",
|
|
27
27
|
"minimist": "^1.2.8",
|
|
28
|
-
"@forgehive/record-tape": "0.0.2",
|
|
29
|
-
"@forgehive/runner": "0.1.5",
|
|
30
28
|
"@forgehive/task": "0.1.5",
|
|
31
|
-
"@forgehive/schema": "0.1.4"
|
|
29
|
+
"@forgehive/schema": "0.1.4",
|
|
30
|
+
"@forgehive/record-tape": "0.0.2",
|
|
31
|
+
"@forgehive/runner": "0.1.5"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/jest": "^29.5.3",
|
package/src/runner.ts
CHANGED
|
@@ -33,7 +33,7 @@ const runner = new Runner((data: ParsedArgs): CliParsedArguments => {
|
|
|
33
33
|
|
|
34
34
|
return {
|
|
35
35
|
taskName: String(_[0]),
|
|
36
|
-
action:
|
|
36
|
+
action: _[1] ?? '',
|
|
37
37
|
args: filteredObj
|
|
38
38
|
}
|
|
39
39
|
})
|
|
@@ -66,7 +66,7 @@ runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
|
|
|
66
66
|
const { taskName, action, args } = parsedArgs
|
|
67
67
|
|
|
68
68
|
console.log('========================================')
|
|
69
|
-
console.log(
|
|
69
|
+
console.log(`Running: ${taskName} ${action ? action : ''} ${JSON.stringify(args)}`)
|
|
70
70
|
console.log('========================================')
|
|
71
71
|
|
|
72
72
|
const task = runner.getTask(taskName)
|
package/src/tasks/conf/info.ts
CHANGED
|
@@ -22,19 +22,27 @@ export const info = createTask(
|
|
|
22
22
|
async function (_argv, { loadCurrentProfile, readFile }) {
|
|
23
23
|
const packageJsonPath = path.join(__dirname, '../../../package.json')
|
|
24
24
|
|
|
25
|
-
|
|
26
25
|
const packageJsonContent = await readFile(packageJsonPath)
|
|
27
26
|
const packageJson = JSON.parse(packageJsonContent)
|
|
28
27
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
return {
|
|
28
|
+
const info = {
|
|
32
29
|
version: packageJson.version,
|
|
33
|
-
profile: {
|
|
30
|
+
profile: {}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let profile
|
|
34
|
+
try {
|
|
35
|
+
profile = await loadCurrentProfile({})
|
|
36
|
+
|
|
37
|
+
info.profile = {
|
|
34
38
|
name: profile.name,
|
|
35
39
|
url: profile.url,
|
|
36
40
|
apiKey: profile.apiKey
|
|
37
41
|
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.log('No default profile set. Please run forge task:run auth:add to create a profile.')
|
|
38
44
|
}
|
|
45
|
+
|
|
46
|
+
return info
|
|
39
47
|
}
|
|
40
48
|
)
|
package/src/tasks/init.ts
CHANGED
|
@@ -37,17 +37,34 @@ const boundaries = {
|
|
|
37
37
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
38
38
|
publishTask: async (data: any, profile: Profile): Promise<any> => {
|
|
39
39
|
const publishUrl = `${profile.url}/api/tasks/publish`
|
|
40
|
-
|
|
41
|
-
console.log(`Publishing task to ${publishUrl}...`)
|
|
42
40
|
const authToken = `${profile.apiKey}:${profile.apiSecret}`
|
|
43
|
-
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const response = await axios.post(publishUrl, data, {
|
|
44
|
+
headers: {
|
|
45
|
+
Authorization: `Bearer ${authToken}`,
|
|
46
|
+
'Content-Type': 'application/json'
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return response.data
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
52
|
+
} catch (error: any) {
|
|
53
|
+
if (error.response?.data?.error.includes('Bundle size')) {
|
|
54
|
+
throw new Error('Bundle size exceeds the maximum allowed size of 25MB')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
throw new Error('Failed to publish task source code and metadata')
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
uploadBundleWithPresignedUrl: async (presignedUrl: string, bundleContent: Buffer): Promise<any> => {
|
|
62
|
+
const response = await axios.put(presignedUrl, bundleContent, {
|
|
44
63
|
headers: {
|
|
45
|
-
|
|
46
|
-
'Content-Type': 'application/json'
|
|
64
|
+
'Content-Type': 'application/octet-stream'
|
|
47
65
|
}
|
|
48
66
|
})
|
|
49
|
-
|
|
50
|
-
return response.data
|
|
67
|
+
return response.status === 200
|
|
51
68
|
},
|
|
52
69
|
|
|
53
70
|
ensureBuildsFolder: async (): Promise<string> => {
|
|
@@ -74,7 +91,8 @@ export const publish = createTask(
|
|
|
74
91
|
readFileUtf8,
|
|
75
92
|
readFileBinary,
|
|
76
93
|
publishTask,
|
|
77
|
-
loadCurrentProfile
|
|
94
|
+
loadCurrentProfile,
|
|
95
|
+
uploadBundleWithPresignedUrl
|
|
78
96
|
}) {
|
|
79
97
|
const cwd = await getCwd()
|
|
80
98
|
const forgeJson = await loadConf({})
|
|
@@ -91,16 +109,14 @@ export const publish = createTask(
|
|
|
91
109
|
const buildsPath = await ensureBuildsFolder()
|
|
92
110
|
const outputFile = path.join(buildsPath, `${descriptorName}.js`)
|
|
93
111
|
|
|
94
|
-
console.log('entryPoint:', entryPoint)
|
|
95
|
-
console.log('buildsPath:', buildsPath)
|
|
96
|
-
console.log('outputFile:', outputFile)
|
|
97
|
-
|
|
98
112
|
// Bundle the task
|
|
99
113
|
await bundleCreate({
|
|
100
114
|
entryPoint,
|
|
101
115
|
outputFile
|
|
102
116
|
})
|
|
103
117
|
|
|
118
|
+
console.log('Bundle created...')
|
|
119
|
+
|
|
104
120
|
// Load the bundled task
|
|
105
121
|
const bundle = await bundleLoad({
|
|
106
122
|
bundlePath: outputFile
|
|
@@ -110,26 +126,46 @@ export const publish = createTask(
|
|
|
110
126
|
const task = bundle[taskDescriptor.handler]
|
|
111
127
|
const description = task.getDescription() ?? ''
|
|
112
128
|
const schema = task.getSchema() || new Schema({})
|
|
129
|
+
const boundaries = Object.keys(task.getBoundaries()) || []
|
|
113
130
|
const schemaDescriptor = schema.describe()
|
|
114
131
|
|
|
115
|
-
// Read the task file content
|
|
132
|
+
// Read the task file content
|
|
116
133
|
const sourceCode = await readFileUtf8(entryPoint)
|
|
117
134
|
const bundleContent = await readFileBinary(outputFile)
|
|
118
135
|
|
|
136
|
+
// Get bundle size
|
|
137
|
+
const bundleSize = bundleContent.length
|
|
138
|
+
|
|
139
|
+
// First, publish task metadata and get presigned URL for bundle upload
|
|
119
140
|
const data = {
|
|
120
141
|
...taskDescriptor,
|
|
121
142
|
taskName: descriptorName,
|
|
122
143
|
projectName,
|
|
123
144
|
description,
|
|
124
145
|
schemaDescriptor: JSON.stringify(schemaDescriptor),
|
|
146
|
+
boundaries,
|
|
125
147
|
sourceCode,
|
|
126
|
-
|
|
148
|
+
bundleSize
|
|
127
149
|
}
|
|
128
150
|
|
|
129
|
-
// Publish to hive api server
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
151
|
+
// Publish metadata to hive api server
|
|
152
|
+
console.log(`Publishing metadata and source code to ${profile.url}...`)
|
|
153
|
+
const publishResponse = await publishTask(data, profile)
|
|
154
|
+
|
|
155
|
+
// Upload bundle using the presigned URL
|
|
156
|
+
if (publishResponse.bundleUploadUrl) {
|
|
157
|
+
console.log('Uploading bundle...')
|
|
158
|
+
await uploadBundleWithPresignedUrl(
|
|
159
|
+
publishResponse.bundleUploadUrl,
|
|
160
|
+
bundleContent
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
descriptor: taskDescriptor,
|
|
165
|
+
publish: true
|
|
166
|
+
}
|
|
167
|
+
} else {
|
|
168
|
+
throw new Error('Bundle upload failed')
|
|
169
|
+
}
|
|
134
170
|
}
|
|
135
171
|
)
|
package/src/tasks/task/run.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import path from 'path'
|
|
6
6
|
import fs from 'fs/promises'
|
|
7
7
|
import os from 'os'
|
|
8
|
+
import axios from 'axios'
|
|
8
9
|
|
|
9
10
|
import { createTask } from '@forgehive/task'
|
|
10
11
|
import { Schema } from '@forgehive/schema'
|
|
@@ -13,7 +14,8 @@ import { RecordTape } from '@forgehive/record-tape'
|
|
|
13
14
|
import { create as bundleCreate } from '../bundle/create'
|
|
14
15
|
import { load as bundleLoad } from '../bundle/load'
|
|
15
16
|
import { load as loadConf } from '../conf/load'
|
|
16
|
-
import {
|
|
17
|
+
import { loadCurrent as loadCurrentProfile } from '../auth/loadCurrent'
|
|
18
|
+
import { type ForgeConf, type Profile } from '../types'
|
|
17
19
|
|
|
18
20
|
// For now, we'll use a simple schema without the record type
|
|
19
21
|
// TODO: Use Schema.record once it's properly built and available
|
|
@@ -25,6 +27,7 @@ const schema = new Schema({
|
|
|
25
27
|
|
|
26
28
|
const boundaries = {
|
|
27
29
|
loadConf: loadConf.asBoundary(),
|
|
30
|
+
loadCurrentProfile: loadCurrentProfile.asBoundary(),
|
|
28
31
|
bundleCreate: bundleCreate.asBoundary(),
|
|
29
32
|
bundleLoad: bundleLoad.asBoundary(),
|
|
30
33
|
verifyLogFolder: async (logsPath: string): Promise<boolean> => {
|
|
@@ -46,21 +49,65 @@ const boundaries = {
|
|
|
46
49
|
}
|
|
47
50
|
|
|
48
51
|
return buildsPath
|
|
52
|
+
},
|
|
53
|
+
sendLogToAPI: async (profile: Profile, projectName: string, taskName: string, logItem: unknown): Promise<boolean> => {
|
|
54
|
+
try {
|
|
55
|
+
const logsUrl = `${profile.url}/api/tasks/log-ingest`
|
|
56
|
+
const authToken = `${profile.apiKey}:${profile.apiSecret}`
|
|
57
|
+
|
|
58
|
+
await axios.post(logsUrl, {
|
|
59
|
+
projectName,
|
|
60
|
+
taskName,
|
|
61
|
+
logItem: JSON.stringify(logItem)
|
|
62
|
+
}, {
|
|
63
|
+
headers: {
|
|
64
|
+
Authorization: `Bearer ${authToken}`,
|
|
65
|
+
'Content-Type': 'application/json'
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
console.log('===============================================')
|
|
70
|
+
console.log('Log sent to API... ', profile.name, profile.url)
|
|
71
|
+
|
|
72
|
+
return true
|
|
73
|
+
} catch (e) {
|
|
74
|
+
const error = e as Error
|
|
75
|
+
console.error('Failed to send log to API:', error.message)
|
|
76
|
+
return false
|
|
77
|
+
}
|
|
49
78
|
}
|
|
50
79
|
}
|
|
51
80
|
|
|
52
81
|
export const run = createTask(
|
|
53
82
|
schema,
|
|
54
83
|
boundaries,
|
|
55
|
-
async function ({ descriptorName, args }, {
|
|
84
|
+
async function ({ descriptorName, args }, {
|
|
85
|
+
loadConf,
|
|
86
|
+
bundleCreate,
|
|
87
|
+
bundleLoad,
|
|
88
|
+
verifyLogFolder,
|
|
89
|
+
ensureBuildsFolder,
|
|
90
|
+
loadCurrentProfile,
|
|
91
|
+
sendLogToAPI
|
|
92
|
+
}) {
|
|
56
93
|
// Load forge configuration
|
|
57
94
|
const forge: ForgeConf = await loadConf({})
|
|
58
95
|
const taskDescriptor = forge.tasks[descriptorName as keyof typeof forge.tasks]
|
|
96
|
+
const projectName = forge.project.name
|
|
59
97
|
|
|
60
98
|
if (taskDescriptor === undefined) {
|
|
61
99
|
throw new Error('Task is not defined on forge.json')
|
|
62
100
|
}
|
|
63
101
|
|
|
102
|
+
// Try to load profile, but continue if not found
|
|
103
|
+
let profile = null
|
|
104
|
+
try {
|
|
105
|
+
profile = await loadCurrentProfile({})
|
|
106
|
+
} catch (error) {
|
|
107
|
+
// Profile not found or not configured, continue without it
|
|
108
|
+
console.log('No profile found, logs will not be sent to remote API')
|
|
109
|
+
}
|
|
110
|
+
|
|
64
111
|
// Verify if log folder exists
|
|
65
112
|
const logFolderPath = path.join(process.cwd(), forge.paths.logs)
|
|
66
113
|
const logFolderExists = await verifyLogFolder(logFolderPath)
|
|
@@ -122,15 +169,28 @@ export const run = createTask(
|
|
|
122
169
|
tape.recordFrom(descriptorName, task)
|
|
123
170
|
|
|
124
171
|
// Run the task with provided arguments
|
|
125
|
-
let result
|
|
172
|
+
let result, error
|
|
126
173
|
try {
|
|
127
174
|
result = await task.run(args)
|
|
128
|
-
} catch (
|
|
129
|
-
|
|
130
|
-
throw error
|
|
175
|
+
} catch (e) {
|
|
176
|
+
error = e as Error
|
|
131
177
|
}
|
|
132
178
|
|
|
133
179
|
await tape.save()
|
|
180
|
+
const logItems = tape.getLog()
|
|
181
|
+
const lastLogItem = logItems[logItems.length - 1]
|
|
182
|
+
|
|
183
|
+
if (profile) {
|
|
184
|
+
try {
|
|
185
|
+
await sendLogToAPI(profile, projectName, descriptorName, lastLogItem)
|
|
186
|
+
} catch (e) {
|
|
187
|
+
console.error('Failed to send log to API:', e)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (error) {
|
|
192
|
+
throw error
|
|
193
|
+
}
|
|
134
194
|
|
|
135
195
|
return result
|
|
136
196
|
}
|
|
@@ -48,7 +48,7 @@ describe('Init task', () => {
|
|
|
48
48
|
const config = JSON.parse(fileContent)
|
|
49
49
|
|
|
50
50
|
// Verify the file content
|
|
51
|
-
expect(config).toHaveProperty('project.name', '
|
|
51
|
+
expect(config).toHaveProperty('project.name', 'BaseProject')
|
|
52
52
|
expect(config).toHaveProperty('paths.logs', 'logs/')
|
|
53
53
|
expect(config).toHaveProperty('paths.tasks', 'src/tasks/')
|
|
54
54
|
expect(config).toHaveProperty('infra.region', 'us-west-2')
|
|
@@ -77,7 +77,7 @@ describe('Init task', () => {
|
|
|
77
77
|
expect(saveFileFn).not.toHaveBeenCalled()
|
|
78
78
|
|
|
79
79
|
// Verify the returned config has the correct structure
|
|
80
|
-
expect(result).toHaveProperty('project.name', '
|
|
80
|
+
expect(result).toHaveProperty('project.name', 'BaseProject')
|
|
81
81
|
expect(result).toHaveProperty('paths.logs', 'logs/')
|
|
82
82
|
expect(result).toHaveProperty('paths.tasks', 'src/tasks/')
|
|
83
83
|
expect(result).toHaveProperty('infra.region', 'us-west-2')
|