@forgehive/forge-cli 0.2.4 → 0.2.6

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.
@@ -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/',
@@ -101,6 +101,7 @@ exports.publish = (0, task_1.createTask)(schema, boundaries, async function ({ d
101
101
  const task = bundle[taskDescriptor.handler];
102
102
  const description = task.getDescription() ?? '';
103
103
  const schema = task.getSchema() || new schema_1.Schema({});
104
+ const boundaries = Object.keys(task.getBoundaries()) || [];
104
105
  const schemaDescriptor = schema.describe();
105
106
  // Read the task file content
106
107
  const sourceCode = await readFileUtf8(entryPoint);
@@ -114,6 +115,7 @@ exports.publish = (0, task_1.createTask)(schema, boundaries, async function ({ d
114
115
  projectName,
115
116
  description,
116
117
  schemaDescriptor: JSON.stringify(schemaDescriptor),
118
+ boundaries,
117
119
  sourceCode,
118
120
  bundleSize
119
121
  };
@@ -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
  });
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const createTask_1 = require("../../tasks/task/createTask");
7
7
  const memfs_1 = require("memfs");
8
8
  const path_1 = __importDefault(require("path"));
9
- const utils_1 = require("../utils");
9
+ const testUtils_1 = require("../testUtils");
10
10
  // Verify the task file content
11
11
  const expectedContent = `// TASK: newTask
12
12
  // Run this task with:
@@ -58,32 +58,31 @@ describe('Create task', () => {
58
58
  });
59
59
  afterEach(() => {
60
60
  jest.clearAllMocks();
61
+ // Reset any boundary mocks
62
+ createTask_1.createTaskCommand.resetMocks();
61
63
  });
62
64
  it('should create a new task file with correct content and update forge.json', async () => {
63
- // Create properly typed mocks for the boundaries
64
- const persistTaskMock = (0, utils_1.createBoundaryMock)();
65
- const persistConfMock = (0, utils_1.createBoundaryMock)();
66
- const getCwdMock = (0, utils_1.createBoundaryMock)();
67
- const persistTaskFn = persistTaskMock;
68
- const persistConfFn = persistConfMock;
69
- const getCwdFn = getCwdMock;
70
- // Override the persistTask implementation to use our in-memory fs
71
- persistTaskFn.mockImplementation(async (dir, fileName, content, cwd) => {
65
+ // Create mock functions with Jest
66
+ const persistTaskFn = jest.fn().mockImplementation(async (dir, fileName, content, cwd) => {
72
67
  const fullPath = path_1.default.join(cwd, dir, fileName);
73
68
  await fs.promises.writeFile(fullPath, content);
74
69
  return { path: fullPath };
75
70
  });
76
- // Override the persistConf implementation to use our in-memory fs
77
- persistConfFn.mockImplementation(async (conf, cwd) => {
71
+ // Mock persistConf to use our in-memory fs
72
+ const persistConfFn = jest.fn().mockImplementation(async (conf, cwd) => {
78
73
  const forgePath = path_1.default.join(cwd, 'forge.json');
79
74
  await fs.promises.writeFile(forgePath, JSON.stringify(conf, null, 2));
80
75
  });
81
- // Override the getCwd implementation to return our root directory
82
- getCwdFn.mockResolvedValue(rootDir);
83
- // Override the boundaries
84
- createTask_1.createTaskCommand.getBoundaries().persistTask = persistTaskMock;
85
- createTask_1.createTaskCommand.getBoundaries().persistConf = persistConfMock;
86
- createTask_1.createTaskCommand.getBoundaries().getCwd = getCwdMock;
76
+ // Mock getCwd to return our root directory
77
+ const getCwdFn = jest.fn().mockResolvedValue(rootDir);
78
+ // Create boundary mocks with proper type casting
79
+ const persistTaskMock = (0, testUtils_1.createMockBoundary)(persistTaskFn);
80
+ const persistConfMock = (0, testUtils_1.createMockBoundary)(persistConfFn);
81
+ const getCwdMock = (0, testUtils_1.createMockBoundary)(getCwdFn);
82
+ // Use the mockBoundary method to mock the boundaries
83
+ createTask_1.createTaskCommand.mockBoundary('persistTask', persistTaskMock);
84
+ createTask_1.createTaskCommand.mockBoundary('persistConf', persistConfMock);
85
+ createTask_1.createTaskCommand.mockBoundary('getCwd', getCwdMock);
87
86
  // Run the task
88
87
  const taskName = 'sample:new-task';
89
88
  await createTask_1.createTaskCommand.run({ descriptorName: taskName });
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const init_1 = require("../../tasks/init");
7
7
  const memfs_1 = require("memfs");
8
8
  const path_1 = __importDefault(require("path"));
9
- const utils_1 = require("../utils");
9
+ const testUtils_1 = require("../testUtils");
10
10
  describe('Init task', () => {
11
11
  let volume;
12
12
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -20,30 +20,29 @@ describe('Init task', () => {
20
20
  });
21
21
  afterEach(() => {
22
22
  jest.clearAllMocks();
23
+ // Reset any boundary mocks after each test
24
+ init_1.init.resetMocks();
23
25
  });
24
26
  it('should create forge.json with correct content in the filesystem', async () => {
25
- // Create properly typed mocks for the boundaries
26
- const saveFileMock = (0, utils_1.createBoundaryMock)();
27
- const getCwdMock = (0, utils_1.createBoundaryMock)();
28
- const saveFileFn = saveFileMock;
29
- const getCwdFn = getCwdMock;
30
- // Override the saveFile implementation to use our in-memory fs
31
- saveFileFn.mockImplementation(async (filePath, content) => {
27
+ // Create mocks directly using Jest
28
+ const saveFileFn = jest.fn().mockImplementation(async (filePath, content) => {
32
29
  const fullPath = path_1.default.join(rootDir, filePath);
33
30
  await fs.promises.writeFile(fullPath, content);
34
31
  });
35
- // Override the getCwd implementation to return our root directory
36
- getCwdFn.mockResolvedValue(rootDir);
37
- // Override the boundaries
38
- init_1.init.getBoundaries().saveFile = saveFileMock;
39
- init_1.init.getBoundaries().getCwd = getCwdMock;
32
+ const getCwdFn = jest.fn().mockResolvedValue(rootDir);
33
+ // Create wrapped boundary mocks
34
+ const saveFileMock = (0, testUtils_1.createMockBoundary)(saveFileFn);
35
+ const getCwdMock = (0, testUtils_1.createMockBoundary)(getCwdFn);
36
+ // Mock the boundaries
37
+ init_1.init.mockBoundary('saveFile', saveFileMock);
38
+ init_1.init.mockBoundary('getCwd', getCwdMock);
40
39
  // Run the task
41
40
  await init_1.init.run({});
42
41
  // Read the created file
43
42
  const fileContent = await fs.promises.readFile(path_1.default.join(rootDir, 'forge.json'), 'utf-8');
44
43
  const config = JSON.parse(fileContent);
45
44
  // Verify the file content
46
- expect(config).toHaveProperty('project.name', 'ChangeMePls');
45
+ expect(config).toHaveProperty('project.name', 'BaseProject');
47
46
  expect(config).toHaveProperty('paths.logs', 'logs/');
48
47
  expect(config).toHaveProperty('paths.tasks', 'src/tasks/');
49
48
  expect(config).toHaveProperty('infra.region', 'us-west-2');
@@ -51,22 +50,21 @@ describe('Init task', () => {
51
50
  expect(config).toHaveProperty('runners');
52
51
  });
53
52
  it('should not create forge.json when dryRun is true', async () => {
54
- // Create properly typed mocks for the boundaries
55
- const saveFileMock = (0, utils_1.createBoundaryMock)();
56
- const getCwdMock = (0, utils_1.createBoundaryMock)();
57
- const saveFileFn = saveFileMock;
58
- const getCwdFn = getCwdMock;
59
- // Override the getCwd implementation to return our root directory
60
- getCwdFn.mockResolvedValue(rootDir);
61
- // Override the boundaries
62
- init_1.init.getBoundaries().saveFile = saveFileMock;
63
- init_1.init.getBoundaries().getCwd = getCwdMock;
53
+ // Create mocks directly using Jest
54
+ const saveFileFn = jest.fn();
55
+ const getCwdFn = jest.fn().mockResolvedValue(rootDir);
56
+ // Create wrapped boundary mocks
57
+ const saveFileMock = (0, testUtils_1.createMockBoundary)(saveFileFn);
58
+ const getCwdMock = (0, testUtils_1.createMockBoundary)(getCwdFn);
59
+ // Mock the boundaries
60
+ init_1.init.mockBoundary('saveFile', saveFileMock);
61
+ init_1.init.mockBoundary('getCwd', getCwdMock);
64
62
  // Run the task with dryRun flag
65
63
  const result = await init_1.init.run({ dryRun: true });
66
64
  // Verify saveFile was not called
67
65
  expect(saveFileFn).not.toHaveBeenCalled();
68
66
  // Verify the returned config has the correct structure
69
- expect(result).toHaveProperty('project.name', 'ChangeMePls');
67
+ expect(result).toHaveProperty('project.name', 'BaseProject');
70
68
  expect(result).toHaveProperty('paths.logs', 'logs/');
71
69
  expect(result).toHaveProperty('paths.tasks', 'src/tasks/');
72
70
  expect(result).toHaveProperty('infra.region', 'us-west-2');
@@ -0,0 +1,8 @@
1
+ import { type WrappedBoundaryFunction } from '@forgehive/task';
2
+ /**
3
+ * Creates a mock boundary function that implements the WrappedBoundaryFunction interface
4
+ *
5
+ * @param mockFn Optional Jest mock function to use as the base function
6
+ * @returns A wrapped boundary function compatible with task.mockBoundary()
7
+ */
8
+ export declare const createMockBoundary: (mockFn?: jest.Mock) => WrappedBoundaryFunction;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createMockBoundary = void 0;
4
+ /**
5
+ * Creates a mock boundary function that implements the WrappedBoundaryFunction interface
6
+ *
7
+ * @param mockFn Optional Jest mock function to use as the base function
8
+ * @returns A wrapped boundary function compatible with task.mockBoundary()
9
+ */
10
+ const createMockBoundary = (mockFn) => {
11
+ // Use provided mock or create a new one
12
+ const baseMockFn = mockFn || jest.fn().mockResolvedValue(undefined);
13
+ // Create a proper boundary function object that extends the mock function
14
+ const boundaryMock = Object.assign(baseMockFn, {
15
+ getTape: jest.fn().mockReturnValue([]),
16
+ setTape: jest.fn(),
17
+ getMode: jest.fn().mockReturnValue('proxy'),
18
+ setMode: jest.fn(),
19
+ startRun: jest.fn(),
20
+ stopRun: jest.fn(),
21
+ getRunData: jest.fn().mockReturnValue([])
22
+ });
23
+ return boundaryMock;
24
+ };
25
+ exports.createMockBoundary = createMockBoundary;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgehive/forge-cli",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "TypeScript CLI application",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -13,7 +13,7 @@
13
13
  "@forgehive/record-tape": "^0.0.1",
14
14
  "@forgehive/runner": "^0.1.4",
15
15
  "@forgehive/schema": "^0.1.4",
16
- "@forgehive/task": "^0.1.5",
16
+ "@forgehive/task": "^0.1.6",
17
17
  "esbuild": "^0.25.0",
18
18
  "handlebars": "^4.7.8",
19
19
  "minimist": "^1.2.8"
@@ -25,10 +25,10 @@
25
25
  "esbuild": "^0.25.0",
26
26
  "handlebars": "^4.7.8",
27
27
  "minimist": "^1.2.8",
28
- "@forgehive/task": "0.1.5",
29
- "@forgehive/record-tape": "0.0.2",
30
- "@forgehive/runner": "0.1.5",
31
- "@forgehive/schema": "0.1.4"
28
+ "@forgehive/record-tape": "0.0.3",
29
+ "@forgehive/runner": "0.1.6",
30
+ "@forgehive/schema": "0.1.4",
31
+ "@forgehive/task": "0.1.6"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/jest": "^29.5.3",
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/',
@@ -126,6 +126,7 @@ export const publish = createTask(
126
126
  const task = bundle[taskDescriptor.handler]
127
127
  const description = task.getDescription() ?? ''
128
128
  const schema = task.getSchema() || new Schema({})
129
+ const boundaries = Object.keys(task.getBoundaries()) || []
129
130
  const schemaDescriptor = schema.describe()
130
131
 
131
132
  // Read the task file content
@@ -142,6 +143,7 @@ export const publish = createTask(
142
143
  projectName,
143
144
  description,
144
145
  schemaDescriptor: JSON.stringify(schemaDescriptor),
146
+ boundaries,
145
147
  sourceCode,
146
148
  bundleSize
147
149
  }
@@ -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
  }
@@ -1,7 +1,7 @@
1
1
  import { createTaskCommand } from '../../tasks/task/createTask'
2
2
  import { createFsFromVolume, Volume } from 'memfs'
3
3
  import path from 'path'
4
- import { createBoundaryMock } from '../utils'
4
+ import { createMockBoundary } from '../testUtils'
5
5
  import { ForgeConf } from '../../tasks/types'
6
6
 
7
7
  // Verify the task file content
@@ -59,37 +59,36 @@ describe('Create task', () => {
59
59
 
60
60
  afterEach(() => {
61
61
  jest.clearAllMocks()
62
+ // Reset any boundary mocks
63
+ createTaskCommand.resetMocks()
62
64
  })
63
65
 
64
66
  it('should create a new task file with correct content and update forge.json', async () => {
65
- // Create properly typed mocks for the boundaries
66
- const persistTaskMock = createBoundaryMock()
67
- const persistConfMock = createBoundaryMock()
68
- const getCwdMock = createBoundaryMock()
69
- const persistTaskFn = persistTaskMock as unknown as jest.Mock
70
- const persistConfFn = persistConfMock as unknown as jest.Mock
71
- const getCwdFn = getCwdMock as unknown as jest.Mock
72
-
73
- // Override the persistTask implementation to use our in-memory fs
74
- persistTaskFn.mockImplementation(async (dir: string, fileName: string, content: string, cwd: string) => {
67
+ // Create mock functions with Jest
68
+ const persistTaskFn = jest.fn().mockImplementation(async (dir: string, fileName: string, content: string, cwd: string) => {
75
69
  const fullPath = path.join(cwd, dir, fileName)
76
70
  await (fs as { promises: { writeFile: (path: string, content: string) => Promise<void> } }).promises.writeFile(fullPath, content)
77
71
  return { path: fullPath }
78
72
  })
79
73
 
80
- // Override the persistConf implementation to use our in-memory fs
81
- persistConfFn.mockImplementation(async (conf: ForgeConf, cwd: string) => {
74
+ // Mock persistConf to use our in-memory fs
75
+ const persistConfFn = jest.fn().mockImplementation(async (conf: ForgeConf, cwd: string) => {
82
76
  const forgePath = path.join(cwd, 'forge.json')
83
77
  await (fs as { promises: { writeFile: (path: string, content: string) => Promise<void> } }).promises.writeFile(forgePath, JSON.stringify(conf, null, 2))
84
78
  })
85
79
 
86
- // Override the getCwd implementation to return our root directory
87
- getCwdFn.mockResolvedValue(rootDir)
80
+ // Mock getCwd to return our root directory
81
+ const getCwdFn = jest.fn().mockResolvedValue(rootDir)
88
82
 
89
- // Override the boundaries
90
- createTaskCommand.getBoundaries().persistTask = persistTaskMock
91
- createTaskCommand.getBoundaries().persistConf = persistConfMock
92
- createTaskCommand.getBoundaries().getCwd = getCwdMock
83
+ // Create boundary mocks with proper type casting
84
+ const persistTaskMock = createMockBoundary(persistTaskFn)
85
+ const persistConfMock = createMockBoundary(persistConfFn)
86
+ const getCwdMock = createMockBoundary(getCwdFn)
87
+
88
+ // Use the mockBoundary method to mock the boundaries
89
+ createTaskCommand.mockBoundary('persistTask', persistTaskMock)
90
+ createTaskCommand.mockBoundary('persistConf', persistConfMock)
91
+ createTaskCommand.mockBoundary('getCwd', getCwdMock)
93
92
 
94
93
  // Run the task
95
94
  const taskName = 'sample:new-task'
@@ -1,7 +1,7 @@
1
1
  import { init } from '../../tasks/init'
2
2
  import { createFsFromVolume, Volume } from 'memfs'
3
3
  import path from 'path'
4
- import { createBoundaryMock } from '../utils'
4
+ import { createMockBoundary } from '../testUtils'
5
5
 
6
6
  describe('Init task', () => {
7
7
  let volume: InstanceType<typeof Volume>
@@ -18,27 +18,26 @@ describe('Init task', () => {
18
18
 
19
19
  afterEach(() => {
20
20
  jest.clearAllMocks()
21
+ // Reset any boundary mocks after each test
22
+ init.resetMocks()
21
23
  })
22
24
 
23
25
  it('should create forge.json with correct content in the filesystem', async () => {
24
- // Create properly typed mocks for the boundaries
25
- const saveFileMock = createBoundaryMock()
26
- const getCwdMock = createBoundaryMock()
27
- const saveFileFn = saveFileMock as unknown as jest.Mock
28
- const getCwdFn = getCwdMock as unknown as jest.Mock
29
-
30
- // Override the saveFile implementation to use our in-memory fs
31
- saveFileFn.mockImplementation(async (filePath: string, content: string) => {
26
+ // Create mocks directly using Jest
27
+ const saveFileFn = jest.fn().mockImplementation(async (filePath: string, content: string) => {
32
28
  const fullPath = path.join(rootDir, filePath)
33
29
  await (fs as { promises: { writeFile: (path: string, content: string) => Promise<void> } }).promises.writeFile(fullPath, content)
34
30
  })
35
31
 
36
- // Override the getCwd implementation to return our root directory
37
- getCwdFn.mockResolvedValue(rootDir)
32
+ const getCwdFn = jest.fn().mockResolvedValue(rootDir)
38
33
 
39
- // Override the boundaries
40
- init.getBoundaries().saveFile = saveFileMock
41
- init.getBoundaries().getCwd = getCwdMock
34
+ // Create wrapped boundary mocks
35
+ const saveFileMock = createMockBoundary(saveFileFn)
36
+ const getCwdMock = createMockBoundary(getCwdFn)
37
+
38
+ // Mock the boundaries
39
+ init.mockBoundary('saveFile', saveFileMock)
40
+ init.mockBoundary('getCwd', getCwdMock)
42
41
 
43
42
  // Run the task
44
43
  await init.run({})
@@ -48,7 +47,7 @@ describe('Init task', () => {
48
47
  const config = JSON.parse(fileContent)
49
48
 
50
49
  // Verify the file content
51
- expect(config).toHaveProperty('project.name', 'ChangeMePls')
50
+ expect(config).toHaveProperty('project.name', 'BaseProject')
52
51
  expect(config).toHaveProperty('paths.logs', 'logs/')
53
52
  expect(config).toHaveProperty('paths.tasks', 'src/tasks/')
54
53
  expect(config).toHaveProperty('infra.region', 'us-west-2')
@@ -57,18 +56,17 @@ describe('Init task', () => {
57
56
  })
58
57
 
59
58
  it('should not create forge.json when dryRun is true', async () => {
60
- // Create properly typed mocks for the boundaries
61
- const saveFileMock = createBoundaryMock()
62
- const getCwdMock = createBoundaryMock()
63
- const saveFileFn = saveFileMock as unknown as jest.Mock
64
- const getCwdFn = getCwdMock as unknown as jest.Mock
59
+ // Create mocks directly using Jest
60
+ const saveFileFn = jest.fn()
61
+ const getCwdFn = jest.fn().mockResolvedValue(rootDir)
65
62
 
66
- // Override the getCwd implementation to return our root directory
67
- getCwdFn.mockResolvedValue(rootDir)
63
+ // Create wrapped boundary mocks
64
+ const saveFileMock = createMockBoundary(saveFileFn)
65
+ const getCwdMock = createMockBoundary(getCwdFn)
68
66
 
69
- // Override the boundaries
70
- init.getBoundaries().saveFile = saveFileMock
71
- init.getBoundaries().getCwd = getCwdMock
67
+ // Mock the boundaries
68
+ init.mockBoundary('saveFile', saveFileMock)
69
+ init.mockBoundary('getCwd', getCwdMock)
72
70
 
73
71
  // Run the task with dryRun flag
74
72
  const result = await init.run({ dryRun: true })
@@ -77,7 +75,7 @@ describe('Init task', () => {
77
75
  expect(saveFileFn).not.toHaveBeenCalled()
78
76
 
79
77
  // Verify the returned config has the correct structure
80
- expect(result).toHaveProperty('project.name', 'ChangeMePls')
78
+ expect(result).toHaveProperty('project.name', 'BaseProject')
81
79
  expect(result).toHaveProperty('paths.logs', 'logs/')
82
80
  expect(result).toHaveProperty('paths.tasks', 'src/tasks/')
83
81
  expect(result).toHaveProperty('infra.region', 'us-west-2')
@@ -0,0 +1,28 @@
1
+ import { type WrappedBoundaryFunction } from '@forgehive/task'
2
+
3
+ /**
4
+ * Creates a mock boundary function that implements the WrappedBoundaryFunction interface
5
+ *
6
+ * @param mockFn Optional Jest mock function to use as the base function
7
+ * @returns A wrapped boundary function compatible with task.mockBoundary()
8
+ */
9
+ export const createMockBoundary = (mockFn?: jest.Mock): WrappedBoundaryFunction => {
10
+ // Use provided mock or create a new one
11
+ const baseMockFn = mockFn || jest.fn().mockResolvedValue(undefined)
12
+
13
+ // Create a proper boundary function object that extends the mock function
14
+ const boundaryMock = Object.assign(
15
+ baseMockFn,
16
+ {
17
+ getTape: jest.fn().mockReturnValue([]),
18
+ setTape: jest.fn(),
19
+ getMode: jest.fn().mockReturnValue('proxy'),
20
+ setMode: jest.fn(),
21
+ startRun: jest.fn(),
22
+ stopRun: jest.fn(),
23
+ getRunData: jest.fn().mockReturnValue([])
24
+ }
25
+ ) as WrappedBoundaryFunction
26
+
27
+ return boundaryMock
28
+ }
package/src/test/utils.ts DELETED
@@ -1,17 +0,0 @@
1
- import { type WrappedBoundaryFunction } from '@forgehive/task'
2
-
3
- export const createBoundaryMock = (): WrappedBoundaryFunction => {
4
- const mockFn = jest.fn().mockResolvedValue(undefined)
5
- const boundaryMock = mockFn as unknown as WrappedBoundaryFunction
6
-
7
- // Add required methods to satisfy the interface
8
- boundaryMock.getTape = jest.fn().mockReturnValue([])
9
- boundaryMock.setTape = jest.fn()
10
- boundaryMock.getMode = jest.fn().mockReturnValue('proxy')
11
- boundaryMock.setMode = jest.fn()
12
- boundaryMock.startRun = jest.fn()
13
- boundaryMock.stopRun = jest.fn()
14
- boundaryMock.getRunData = jest.fn().mockReturnValue([])
15
-
16
- return boundaryMock
17
- }