@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 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: String(_[1]),
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('Running:', taskName, action, args);
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>>;
@@ -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 profile = await loadCurrentProfile({});
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
  });
@@ -27,7 +27,7 @@ exports.init = (0, task_1.createTask)(schema, boundaries, async function (argv,
27
27
  const forgePath = path_1.default.join(cwd, 'forge.json');
28
28
  const config = {
29
29
  project: {
30
- name: 'ChangeMePls'
30
+ name: 'BaseProject'
31
31
  },
32
32
  paths: {
33
33
  logs: 'logs/',
@@ -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
- publishResponse: any;
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
- const response = await axios_1.default.post(publishUrl, data, {
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
- Authorization: `Bearer ${authToken}`,
45
- 'Content-Type': 'application/json'
62
+ 'Content-Type': 'application/octet-stream'
46
63
  }
47
64
  });
48
- return response.data;
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 and bundle
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
- bundle: bundleContent.toString('base64')
120
+ bundleSize
101
121
  };
102
- // Publish to hive api server
103
- const response = await publishTask(data, profile);
104
- console.log('Publish response:', response);
105
- return { descriptor: taskDescriptor, publishResponse: response };
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
  });
@@ -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
  }>;
@@ -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 (error) {
113
- await tape.save();
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', 'ChangeMePls');
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', 'ChangeMePls');
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",
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: String(_[1]),
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('Running:', taskName, action, args)
69
+ console.log(`Running: ${taskName} ${action ? action : ''} ${JSON.stringify(args)}`)
70
70
  console.log('========================================')
71
71
 
72
72
  const task = runner.getTask(taskName)
@@ -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 profile = await loadCurrentProfile({})
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
@@ -32,7 +32,7 @@ export const init = createTask(
32
32
 
33
33
  const config: ForgeConf = {
34
34
  project: {
35
- name: 'ChangeMePls'
35
+ name: 'BaseProject'
36
36
  },
37
37
  paths: {
38
38
  logs: 'logs/',
@@ -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
- const response = await axios.post(publishUrl, data, {
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
- Authorization: `Bearer ${authToken}`,
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 and bundle
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
- bundle: bundleContent.toString('base64')
148
+ bundleSize
127
149
  }
128
150
 
129
- // Publish to hive api server
130
- const response = await publishTask(data, profile)
131
-
132
- console.log('Publish response:', response)
133
- return { descriptor: taskDescriptor, publishResponse: response }
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
  )
@@ -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 { type ForgeConf } from '../types'
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 }, { loadConf, bundleCreate, bundleLoad, verifyLogFolder, ensureBuildsFolder }) {
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 (error) {
129
- await tape.save()
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', 'ChangeMePls')
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', 'ChangeMePls')
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')