@forgehive/forge-cli 0.2.9 → 0.2.11
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/index.js +8 -2
- package/dist/runner.js +25 -2
- package/dist/tasks/fixture/download.d.ts +34 -0
- package/dist/tasks/fixture/download.js +99 -0
- package/dist/tasks/init.js +1 -1
- package/dist/tasks/task/replay.d.ts +49 -0
- package/dist/tasks/task/replay.js +179 -0
- package/forge.json +9 -1
- package/package.json +7 -7
- package/src/index.ts +16 -2
- package/src/runner.ts +27 -2
- package/src/tasks/fixture/download.ts +136 -0
- package/src/tasks/init.ts +1 -1
- package/src/tasks/task/replay.ts +220 -0
package/dist/index.js
CHANGED
|
@@ -7,9 +7,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
const minimist_1 = __importDefault(require("minimist"));
|
|
8
8
|
const runner_1 = __importDefault(require("./runner"));
|
|
9
9
|
const args = (0, minimist_1.default)(process.argv.slice(2));
|
|
10
|
-
runner_1.default.handler(args).then(data => {
|
|
10
|
+
runner_1.default.handler(args).then((data) => {
|
|
11
|
+
const { silent, outcome, result } = data;
|
|
12
|
+
if (silent) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
11
15
|
console.log('===============================================');
|
|
12
|
-
console.log(
|
|
16
|
+
console.log(`Outcome: ${outcome}`);
|
|
17
|
+
console.log('===============================================');
|
|
18
|
+
console.log('Result', result);
|
|
13
19
|
console.log('===============================================');
|
|
14
20
|
}).catch((e) => {
|
|
15
21
|
console.error(e);
|
package/dist/runner.js
CHANGED
|
@@ -18,6 +18,8 @@ const remove_2 = require("./tasks/runner/remove");
|
|
|
18
18
|
const bundle_1 = require("./tasks/runner/bundle");
|
|
19
19
|
const publish_1 = require("./tasks/task/publish");
|
|
20
20
|
const download_1 = require("./tasks/task/download");
|
|
21
|
+
const replay_1 = require("./tasks/task/replay");
|
|
22
|
+
const download_2 = require("./tasks/fixture/download");
|
|
21
23
|
const add_1 = require("./tasks/auth/add");
|
|
22
24
|
const switch_1 = require("./tasks/auth/switch");
|
|
23
25
|
const list_1 = require("./tasks/auth/list");
|
|
@@ -39,10 +41,13 @@ runner.load('task:run', run_1.run);
|
|
|
39
41
|
runner.load('task:remove', remove_1.remove);
|
|
40
42
|
runner.load('task:publish', publish_1.publish);
|
|
41
43
|
runner.load('task:download', download_1.download);
|
|
44
|
+
runner.load('task:replay', replay_1.replay);
|
|
42
45
|
// Runner commands
|
|
43
46
|
runner.load('runner:create', create_1.create);
|
|
44
47
|
runner.load('runner:remove', remove_2.remove);
|
|
45
48
|
runner.load('runner:bundle', bundle_1.bundle);
|
|
49
|
+
// Fixture commands
|
|
50
|
+
runner.load('fixture:download', download_2.download);
|
|
46
51
|
// Auth commands
|
|
47
52
|
runner.load('auth:add', add_1.add);
|
|
48
53
|
runner.load('auth:switch', switch_1.switchProfile);
|
|
@@ -52,9 +57,10 @@ runner.load('auth:remove', remove_3.remove);
|
|
|
52
57
|
runner.setHandler(async (data) => {
|
|
53
58
|
const parsedArgs = runner.parseArguments(data);
|
|
54
59
|
const { taskName, action, args } = parsedArgs;
|
|
55
|
-
console.log('
|
|
60
|
+
console.log('===============================================');
|
|
56
61
|
console.log(`Running: ${taskName} ${action ? action : ''} ${JSON.stringify(args)}`);
|
|
57
|
-
console.log('
|
|
62
|
+
console.log('===============================================');
|
|
63
|
+
let silent = false;
|
|
58
64
|
const task = runner.getTask(taskName);
|
|
59
65
|
if (!task) {
|
|
60
66
|
throw new Error(`Task "${taskName}" not found`);
|
|
@@ -86,12 +92,28 @@ runner.setHandler(async (data) => {
|
|
|
86
92
|
uuid
|
|
87
93
|
});
|
|
88
94
|
}
|
|
95
|
+
else if (taskName === 'task:replay') {
|
|
96
|
+
const { path, cache } = args;
|
|
97
|
+
result = await task.run({
|
|
98
|
+
descriptorName: action,
|
|
99
|
+
path,
|
|
100
|
+
cache
|
|
101
|
+
});
|
|
102
|
+
}
|
|
89
103
|
else if (taskName === 'task:run') {
|
|
90
104
|
result = await task.run({
|
|
91
105
|
descriptorName: action,
|
|
92
106
|
args
|
|
93
107
|
});
|
|
94
108
|
}
|
|
109
|
+
else if (taskName === 'fixture:download') {
|
|
110
|
+
const { uuid } = args;
|
|
111
|
+
result = await task.run({
|
|
112
|
+
descriptorName: action,
|
|
113
|
+
uuid
|
|
114
|
+
});
|
|
115
|
+
silent = true;
|
|
116
|
+
}
|
|
95
117
|
else if (taskName === 'auth:add') {
|
|
96
118
|
const { apiKey, apiSecret, url } = args;
|
|
97
119
|
result = await task.run({
|
|
@@ -110,6 +132,7 @@ runner.setHandler(async (data) => {
|
|
|
110
132
|
result = await task.run(args);
|
|
111
133
|
}
|
|
112
134
|
return {
|
|
135
|
+
silent,
|
|
113
136
|
outcome: 'Success',
|
|
114
137
|
taskName,
|
|
115
138
|
result
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Profile } from '../types';
|
|
2
|
+
interface FixtureData {
|
|
3
|
+
name: string;
|
|
4
|
+
boundaries: Record<string, unknown>;
|
|
5
|
+
[key: string]: unknown;
|
|
6
|
+
}
|
|
7
|
+
interface FixtureResponse {
|
|
8
|
+
fixture: FixtureData;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
export declare const download: import("@forgehive/task").TaskInstanceType<(argv: {
|
|
12
|
+
uuid: string;
|
|
13
|
+
}, boundaries: import("@forgehive/task").WrappedBoundaries<{
|
|
14
|
+
loadCurrentProfile: (args: {}) => Promise<Promise<Profile>>;
|
|
15
|
+
loadConf: (args: {}) => Promise<Promise<import("../types").ForgeConf>>;
|
|
16
|
+
downloadFixture: (uuid: string, profile: Profile) => Promise<FixtureResponse>;
|
|
17
|
+
getCwd: () => Promise<string>;
|
|
18
|
+
persistFixture: (filePath: string, data: FixtureData) => Promise<{
|
|
19
|
+
path: string;
|
|
20
|
+
}>;
|
|
21
|
+
}>) => Promise<{
|
|
22
|
+
status: string;
|
|
23
|
+
path: string;
|
|
24
|
+
shortPath: string;
|
|
25
|
+
}>, {
|
|
26
|
+
loadCurrentProfile: (args: {}) => Promise<Promise<Profile>>;
|
|
27
|
+
loadConf: (args: {}) => Promise<Promise<import("../types").ForgeConf>>;
|
|
28
|
+
downloadFixture: (uuid: string, profile: Profile) => Promise<FixtureResponse>;
|
|
29
|
+
getCwd: () => Promise<string>;
|
|
30
|
+
persistFixture: (filePath: string, data: FixtureData) => Promise<{
|
|
31
|
+
path: string;
|
|
32
|
+
}>;
|
|
33
|
+
}>;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// TASK: download
|
|
3
|
+
// Run this task with:
|
|
4
|
+
// forge task:run fixture:download --uuid [fixture-uuid]
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.download = void 0;
|
|
10
|
+
const task_1 = require("@forgehive/task");
|
|
11
|
+
const schema_1 = require("@forgehive/schema");
|
|
12
|
+
const axios_1 = __importDefault(require("axios"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
15
|
+
const loadCurrent_1 = require("../auth/loadCurrent");
|
|
16
|
+
const load_1 = require("../conf/load");
|
|
17
|
+
const description = 'Download a fixture by UUID to a path based on task descriptor returned from API';
|
|
18
|
+
const schema = new schema_1.Schema({
|
|
19
|
+
uuid: schema_1.Schema.string()
|
|
20
|
+
});
|
|
21
|
+
const boundaries = {
|
|
22
|
+
loadCurrentProfile: loadCurrent_1.loadCurrent.asBoundary(),
|
|
23
|
+
loadConf: load_1.load.asBoundary(),
|
|
24
|
+
downloadFixture: async (uuid, profile) => {
|
|
25
|
+
const downloadUrl = `${profile.url}/api/fixture/${uuid}`;
|
|
26
|
+
console.log(`Downloading fixture from ${downloadUrl}...`);
|
|
27
|
+
const authToken = `${profile.apiKey}:${profile.apiSecret}`;
|
|
28
|
+
const response = await axios_1.default.get(downloadUrl, {
|
|
29
|
+
headers: {
|
|
30
|
+
Authorization: `Bearer ${authToken}`,
|
|
31
|
+
'Content-Type': 'application/json'
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return response.data;
|
|
35
|
+
},
|
|
36
|
+
getCwd: async () => {
|
|
37
|
+
return process.cwd();
|
|
38
|
+
},
|
|
39
|
+
persistFixture: async (filePath, data) => {
|
|
40
|
+
const dirPath = path_1.default.dirname(filePath);
|
|
41
|
+
await promises_1.default.mkdir(dirPath, { recursive: true });
|
|
42
|
+
await promises_1.default.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
43
|
+
return {
|
|
44
|
+
path: filePath
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
exports.download = (0, task_1.createTask)(schema, boundaries, async function ({ uuid }, { downloadFixture, getCwd, persistFixture, loadCurrentProfile, loadConf }) {
|
|
49
|
+
console.log('==================================================');
|
|
50
|
+
console.log(`Attempting to download fixture with uuid: ${uuid}`);
|
|
51
|
+
console.log('==================================================');
|
|
52
|
+
const profile = await loadCurrentProfile({});
|
|
53
|
+
const cwd = await getCwd();
|
|
54
|
+
const forge = await loadConf({});
|
|
55
|
+
// Download from hive api server first to get the task descriptor
|
|
56
|
+
let response;
|
|
57
|
+
try {
|
|
58
|
+
response = await downloadFixture(uuid, profile);
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
const error = e;
|
|
62
|
+
console.error('Error downloading fixture:', error.message, error.status);
|
|
63
|
+
if (error.status === 404) {
|
|
64
|
+
throw new Error('Fixture not found');
|
|
65
|
+
}
|
|
66
|
+
throw new Error('Failed to download fixture');
|
|
67
|
+
}
|
|
68
|
+
const fixture = response.fixture;
|
|
69
|
+
const taskName = fixture.taskName;
|
|
70
|
+
// Determine the output path using forge fixtures path and task descriptor
|
|
71
|
+
const fixturesBasePath = forge.paths.fixtures || 'fixtures';
|
|
72
|
+
const fixtureDir = path_1.default.join(fixturesBasePath, taskName);
|
|
73
|
+
const fixturePath = path_1.default.join(fixtureDir, `${uuid}.json`);
|
|
74
|
+
const filePath = path_1.default.resolve(cwd, fixturePath);
|
|
75
|
+
// Persist fixture to file
|
|
76
|
+
await persistFixture(filePath, response.fixture);
|
|
77
|
+
// Get the relative path for display in the replay command
|
|
78
|
+
const shortPath = path_1.default.join(taskName, `${uuid}.json`);
|
|
79
|
+
console.log(`
|
|
80
|
+
==================================================
|
|
81
|
+
Fixture download completed!
|
|
82
|
+
==================================================
|
|
83
|
+
Fixture UUID: ${uuid}
|
|
84
|
+
Task Name: ${taskName}
|
|
85
|
+
Saved to: ${filePath}
|
|
86
|
+
==================================================
|
|
87
|
+
Boundaries: ${Object.keys(fixture.boundaries).join(', ')}
|
|
88
|
+
==================================================
|
|
89
|
+
Replay with:
|
|
90
|
+
forge task:replay ${taskName} --path ${shortPath}
|
|
91
|
+
==================================================
|
|
92
|
+
`);
|
|
93
|
+
return {
|
|
94
|
+
status: 'Downloaded',
|
|
95
|
+
path: filePath,
|
|
96
|
+
shortPath: shortPath
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
exports.download.setDescription(description);
|
package/dist/tasks/init.js
CHANGED
|
@@ -31,9 +31,9 @@ exports.init = (0, task_1.createTask)(schema, boundaries, async function (argv,
|
|
|
31
31
|
},
|
|
32
32
|
paths: {
|
|
33
33
|
logs: 'logs/',
|
|
34
|
+
fixtures: 'fixtures/',
|
|
34
35
|
tasks: 'src/tasks/',
|
|
35
36
|
runners: 'src/runners/',
|
|
36
|
-
fixtures: 'src/tests/fixtures',
|
|
37
37
|
tests: 'src/tests/'
|
|
38
38
|
},
|
|
39
39
|
infra: {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type ForgeConf, type Profile } from '../types';
|
|
2
|
+
interface Fixture {
|
|
3
|
+
fixtureUUID: string;
|
|
4
|
+
taskName: string;
|
|
5
|
+
projectName: string;
|
|
6
|
+
type: 'success' | 'error';
|
|
7
|
+
input: Record<string, unknown>;
|
|
8
|
+
output: Record<string, unknown>;
|
|
9
|
+
boundaries: Record<string, unknown>;
|
|
10
|
+
context: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
export declare const replay: import("@forgehive/task").TaskInstanceType<(argv: {
|
|
13
|
+
path: string;
|
|
14
|
+
descriptorName: string;
|
|
15
|
+
cache?: string | undefined;
|
|
16
|
+
}, boundaries: import("@forgehive/task").WrappedBoundaries<{
|
|
17
|
+
readFixture: (filePath: string) => Promise<Fixture>;
|
|
18
|
+
loadConf: (args: {}) => Promise<Promise<ForgeConf>>;
|
|
19
|
+
loadCurrentProfile: (args: {}) => Promise<Promise<Profile>>;
|
|
20
|
+
bundleCreate: (args: {
|
|
21
|
+
entryPoint: string;
|
|
22
|
+
outputFile: string;
|
|
23
|
+
}) => Promise<Promise<{
|
|
24
|
+
outputFile: string;
|
|
25
|
+
}>>;
|
|
26
|
+
bundleLoad: (args: {
|
|
27
|
+
bundlePath: string;
|
|
28
|
+
}) => Promise<Promise<any>>;
|
|
29
|
+
ensureBuildsFolder: () => Promise<string>;
|
|
30
|
+
verifyLogFolder: (logsPath: string) => Promise<boolean>;
|
|
31
|
+
sendLogToAPI: (profile: Profile, projectName: string, taskName: string, logItem: unknown, fixtureUUID: string) => Promise<boolean>;
|
|
32
|
+
}>) => Promise<any>, {
|
|
33
|
+
readFixture: (filePath: string) => Promise<Fixture>;
|
|
34
|
+
loadConf: (args: {}) => Promise<Promise<ForgeConf>>;
|
|
35
|
+
loadCurrentProfile: (args: {}) => Promise<Promise<Profile>>;
|
|
36
|
+
bundleCreate: (args: {
|
|
37
|
+
entryPoint: string;
|
|
38
|
+
outputFile: string;
|
|
39
|
+
}) => Promise<Promise<{
|
|
40
|
+
outputFile: string;
|
|
41
|
+
}>>;
|
|
42
|
+
bundleLoad: (args: {
|
|
43
|
+
bundlePath: string;
|
|
44
|
+
}) => Promise<Promise<any>>;
|
|
45
|
+
ensureBuildsFolder: () => Promise<string>;
|
|
46
|
+
verifyLogFolder: (logsPath: string) => Promise<boolean>;
|
|
47
|
+
sendLogToAPI: (profile: Profile, projectName: string, taskName: string, logItem: unknown, fixtureUUID: string) => Promise<boolean>;
|
|
48
|
+
}>;
|
|
49
|
+
export {};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// TASK: replay
|
|
3
|
+
// Run this task with:
|
|
4
|
+
// forge task:run task:replay
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.replay = void 0;
|
|
10
|
+
const task_1 = require("@forgehive/task");
|
|
11
|
+
const schema_1 = require("@forgehive/schema");
|
|
12
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const os_1 = __importDefault(require("os"));
|
|
15
|
+
const axios_1 = __importDefault(require("axios"));
|
|
16
|
+
const create_1 = require("../bundle/create");
|
|
17
|
+
const load_1 = require("../bundle/load");
|
|
18
|
+
const load_2 = require("../conf/load");
|
|
19
|
+
const loadCurrent_1 = require("../auth/loadCurrent");
|
|
20
|
+
const description = 'Replay a task execution from a specified path';
|
|
21
|
+
const schema = new schema_1.Schema({
|
|
22
|
+
descriptorName: schema_1.Schema.string(),
|
|
23
|
+
path: schema_1.Schema.string(),
|
|
24
|
+
cache: schema_1.Schema.string().optional()
|
|
25
|
+
});
|
|
26
|
+
const boundaries = {
|
|
27
|
+
readFixture: async (filePath) => {
|
|
28
|
+
const fileContent = await promises_1.default.readFile(filePath, 'utf-8');
|
|
29
|
+
const parsedData = JSON.parse(fileContent);
|
|
30
|
+
return parsedData;
|
|
31
|
+
},
|
|
32
|
+
loadConf: load_2.load.asBoundary(),
|
|
33
|
+
loadCurrentProfile: loadCurrent_1.loadCurrent.asBoundary(),
|
|
34
|
+
bundleCreate: create_1.create.asBoundary(),
|
|
35
|
+
bundleLoad: load_1.load.asBoundary(),
|
|
36
|
+
ensureBuildsFolder: async () => {
|
|
37
|
+
const buildsPath = path_1.default.join(os_1.default.homedir(), '.forge', 'builds');
|
|
38
|
+
try {
|
|
39
|
+
await promises_1.default.access(buildsPath);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
await promises_1.default.mkdir(buildsPath, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
return buildsPath;
|
|
45
|
+
},
|
|
46
|
+
verifyLogFolder: async (logsPath) => {
|
|
47
|
+
// return true if the folder exists
|
|
48
|
+
try {
|
|
49
|
+
await promises_1.default.access(logsPath);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
},
|
|
56
|
+
sendLogToAPI: async (profile, projectName, taskName, logItem, fixtureUUID) => {
|
|
57
|
+
try {
|
|
58
|
+
const logsUrl = `${profile.url}/api/tasks/log-ingest`;
|
|
59
|
+
const authToken = `${profile.apiKey}:${profile.apiSecret}`;
|
|
60
|
+
await axios_1.default.post(logsUrl, {
|
|
61
|
+
projectName,
|
|
62
|
+
taskName,
|
|
63
|
+
logItem: JSON.stringify(logItem),
|
|
64
|
+
replayFrom: fixtureUUID
|
|
65
|
+
}, {
|
|
66
|
+
headers: {
|
|
67
|
+
Authorization: `Bearer ${authToken}`,
|
|
68
|
+
'Content-Type': 'application/json'
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
console.log('===============================================');
|
|
72
|
+
console.log('Log sent to API... ', profile.name, profile.url);
|
|
73
|
+
console.log('Replay from fixture UUID:', fixtureUUID);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
const error = e;
|
|
78
|
+
console.error('Failed to send log to API:', error.message);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
exports.replay = (0, task_1.createTask)(schema, boundaries, async function ({ descriptorName, path: fixturePath, cache }, { readFixture, loadConf, loadCurrentProfile, bundleCreate, bundleLoad, ensureBuildsFolder, verifyLogFolder, sendLogToAPI }) {
|
|
84
|
+
console.log('Input descriptorName:', descriptorName);
|
|
85
|
+
console.log('Input path:', fixturePath);
|
|
86
|
+
console.log('Input cache:', cache);
|
|
87
|
+
// Load forge configuration
|
|
88
|
+
const forge = await loadConf({});
|
|
89
|
+
const taskDescriptor = forge.tasks[descriptorName];
|
|
90
|
+
const projectName = forge.project.name;
|
|
91
|
+
if (taskDescriptor === undefined) {
|
|
92
|
+
throw new Error(`Task ${descriptorName} is not defined in forge.json`);
|
|
93
|
+
}
|
|
94
|
+
// Resolve the fixture path (check if absolute, if not make it relative to logs folder)
|
|
95
|
+
const resolvedFixturePath = path_1.default.isAbsolute(fixturePath)
|
|
96
|
+
? fixturePath
|
|
97
|
+
: path_1.default.join(process.cwd(), forge.paths.fixtures, fixturePath);
|
|
98
|
+
// Read the file from the provided path
|
|
99
|
+
const fixture = await readFixture(resolvedFixturePath);
|
|
100
|
+
// Try to load profile, but continue if not found
|
|
101
|
+
let profile = null;
|
|
102
|
+
try {
|
|
103
|
+
profile = await loadCurrentProfile({});
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
// Profile not found or not configured, continue without it
|
|
107
|
+
console.log('No profile found, logs will not be sent to remote API');
|
|
108
|
+
}
|
|
109
|
+
// Verify if log folder exists
|
|
110
|
+
const logFolderPath = path_1.default.join(process.cwd(), forge.paths.logs);
|
|
111
|
+
const logFolderExists = await verifyLogFolder(logFolderPath);
|
|
112
|
+
if (!logFolderExists) {
|
|
113
|
+
throw new Error(`Log folder "${logFolderPath}" does not exist`);
|
|
114
|
+
}
|
|
115
|
+
// Prepare paths
|
|
116
|
+
const entryPoint = path_1.default.join(process.cwd(), taskDescriptor.path);
|
|
117
|
+
const buildsPath = await ensureBuildsFolder();
|
|
118
|
+
const outputFile = path_1.default.join(buildsPath, `${descriptorName}.js`);
|
|
119
|
+
// Bundle the task
|
|
120
|
+
await bundleCreate({
|
|
121
|
+
entryPoint,
|
|
122
|
+
outputFile
|
|
123
|
+
});
|
|
124
|
+
// Load the bundled task
|
|
125
|
+
const bundle = await bundleLoad({
|
|
126
|
+
bundlePath: outputFile
|
|
127
|
+
});
|
|
128
|
+
// Get the task handler
|
|
129
|
+
const task = bundle[taskDescriptor.handler];
|
|
130
|
+
if (!task) {
|
|
131
|
+
throw new Error(`Handler "${taskDescriptor.handler}" not found in bundle`);
|
|
132
|
+
}
|
|
133
|
+
// Configure boundaries based on cache parameter if provided
|
|
134
|
+
const boundaryConfig = {};
|
|
135
|
+
if (cache) {
|
|
136
|
+
// Parse the comma-separated list and trim each item
|
|
137
|
+
const cacheBoundaries = cache.split(',').map((b) => b.trim());
|
|
138
|
+
// Log which boundaries will use cache mode
|
|
139
|
+
if (cacheBoundaries.length > 0) {
|
|
140
|
+
// Set each specified boundary to 'replay' mode
|
|
141
|
+
cacheBoundaries.forEach((boundary) => {
|
|
142
|
+
boundaryConfig[boundary] = 'replay';
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
console.log('==================================================');
|
|
147
|
+
console.log('UUID:', fixture.fixtureUUID);
|
|
148
|
+
console.log('Task name:', fixture.taskName);
|
|
149
|
+
console.log('Project name:', fixture.projectName);
|
|
150
|
+
console.log('Context:', fixture.context);
|
|
151
|
+
console.log('==================================================');
|
|
152
|
+
console.log('Replay:', fixture.input);
|
|
153
|
+
console.log('Boundaries:', JSON.stringify(fixture.boundaries, null, 2));
|
|
154
|
+
console.log('==================================================');
|
|
155
|
+
console.log('Boundary config:', boundaryConfig);
|
|
156
|
+
console.log('==================================================');
|
|
157
|
+
// Perform the replay
|
|
158
|
+
const [result, error, record] = await task.safeReplay({
|
|
159
|
+
input: fixture.input,
|
|
160
|
+
output: fixture.output,
|
|
161
|
+
boundaries: fixture.boundaries,
|
|
162
|
+
}, {
|
|
163
|
+
boundaries: boundaryConfig // Use configured boundary modes
|
|
164
|
+
});
|
|
165
|
+
// Send the log to API if profile is available
|
|
166
|
+
if (profile) {
|
|
167
|
+
try {
|
|
168
|
+
await sendLogToAPI(profile, projectName, descriptorName, record, fixture.fixtureUUID);
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
console.error('Failed to send log to API:', e);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (error) {
|
|
175
|
+
throw new Error(error.message);
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
});
|
|
179
|
+
exports.replay.setDescription(description);
|
package/forge.json
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
},
|
|
5
5
|
"paths": {
|
|
6
6
|
"logs": "logs/",
|
|
7
|
+
"fixtures": "fixtures",
|
|
7
8
|
"tasks": "src/tasks/",
|
|
8
9
|
"runners": "src/runners/",
|
|
9
|
-
"fixtures": "src/tests/fixtures",
|
|
10
10
|
"tests": "src/tests/"
|
|
11
11
|
},
|
|
12
12
|
"infra": {
|
|
@@ -77,6 +77,14 @@
|
|
|
77
77
|
"auth:remove": {
|
|
78
78
|
"path": "src/tasks/auth/remove.ts",
|
|
79
79
|
"handler": "remove"
|
|
80
|
+
},
|
|
81
|
+
"task:replay": {
|
|
82
|
+
"path": "src/tasks/task/replay.ts",
|
|
83
|
+
"handler": "replay"
|
|
84
|
+
},
|
|
85
|
+
"fixture:download": {
|
|
86
|
+
"path": "src/tasks/fixture/download.ts",
|
|
87
|
+
"handler": "download"
|
|
80
88
|
}
|
|
81
89
|
},
|
|
82
90
|
"runners": {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forgehive/forge-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"description": "TypeScript CLI application",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
"publishConfig": {
|
|
11
11
|
"access": "public",
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@forgehive/record-tape": "^0.1.
|
|
14
|
-
"@forgehive/runner": "^0.1.
|
|
13
|
+
"@forgehive/record-tape": "^0.1.4",
|
|
14
|
+
"@forgehive/runner": "^0.1.10",
|
|
15
15
|
"@forgehive/schema": "^0.1.4",
|
|
16
|
-
"@forgehive/task": "^0.1.
|
|
16
|
+
"@forgehive/task": "^0.1.11",
|
|
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/runner": "0.1.
|
|
29
|
-
"@forgehive/record-tape": "0.1.
|
|
28
|
+
"@forgehive/runner": "0.1.10",
|
|
29
|
+
"@forgehive/record-tape": "0.1.4",
|
|
30
30
|
"@forgehive/schema": "0.1.4",
|
|
31
|
-
"@forgehive/task": "0.1.
|
|
31
|
+
"@forgehive/task": "0.1.11"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/jest": "^29.5.3",
|
package/src/index.ts
CHANGED
|
@@ -5,9 +5,23 @@ import runner from './runner'
|
|
|
5
5
|
|
|
6
6
|
const args = minimist(process.argv.slice(2))
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
type RunnerResult = {
|
|
9
|
+
silent: boolean
|
|
10
|
+
outcome: 'Success' | 'Failure'
|
|
11
|
+
taskName: string
|
|
12
|
+
result: unknown
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
runner.handler(args).then((data) => {
|
|
16
|
+
const { silent, outcome, result } = data as RunnerResult
|
|
17
|
+
if (silent) {
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
console.log('===============================================')
|
|
22
|
+
console.log(`Outcome: ${outcome}`)
|
|
9
23
|
console.log('===============================================')
|
|
10
|
-
console.log('
|
|
24
|
+
console.log('Result', result)
|
|
11
25
|
console.log('===============================================')
|
|
12
26
|
}).catch((e) => {
|
|
13
27
|
console.error(e)
|
package/src/runner.ts
CHANGED
|
@@ -18,6 +18,9 @@ import { remove as removeRunner } from './tasks/runner/remove'
|
|
|
18
18
|
import { bundle as bundleRunner } from './tasks/runner/bundle'
|
|
19
19
|
import { publish as publishTask } from './tasks/task/publish'
|
|
20
20
|
import { download as downloadTask } from './tasks/task/download'
|
|
21
|
+
import { replay as replayTask } from './tasks/task/replay'
|
|
22
|
+
|
|
23
|
+
import { download as downloadFixture } from './tasks/fixture/download'
|
|
21
24
|
|
|
22
25
|
import { add as addProfile } from './tasks/auth/add'
|
|
23
26
|
import { switchProfile } from './tasks/auth/switch'
|
|
@@ -48,12 +51,16 @@ runner.load('task:run', taskRunCommand)
|
|
|
48
51
|
runner.load('task:remove', taskRemoveCommand)
|
|
49
52
|
runner.load('task:publish', publishTask)
|
|
50
53
|
runner.load('task:download', downloadTask)
|
|
54
|
+
runner.load('task:replay', replayTask)
|
|
51
55
|
|
|
52
56
|
// Runner commands
|
|
53
57
|
runner.load('runner:create', createRunner)
|
|
54
58
|
runner.load('runner:remove', removeRunner)
|
|
55
59
|
runner.load('runner:bundle', bundleRunner)
|
|
56
60
|
|
|
61
|
+
// Fixture commands
|
|
62
|
+
runner.load('fixture:download', downloadFixture)
|
|
63
|
+
|
|
57
64
|
// Auth commands
|
|
58
65
|
runner.load('auth:add', addProfile)
|
|
59
66
|
runner.load('auth:switch', switchProfile)
|
|
@@ -65,10 +72,11 @@ runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
|
|
|
65
72
|
const parsedArgs = runner.parseArguments(data)
|
|
66
73
|
const { taskName, action, args } = parsedArgs
|
|
67
74
|
|
|
68
|
-
console.log('
|
|
75
|
+
console.log('===============================================')
|
|
69
76
|
console.log(`Running: ${taskName} ${action ? action : ''} ${JSON.stringify(args)}`)
|
|
70
|
-
console.log('
|
|
77
|
+
console.log('===============================================')
|
|
71
78
|
|
|
79
|
+
let silent = false
|
|
72
80
|
const task = runner.getTask(taskName)
|
|
73
81
|
if (!task) {
|
|
74
82
|
throw new Error(`Task "${taskName}" not found`)
|
|
@@ -101,11 +109,27 @@ runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
|
|
|
101
109
|
descriptorName: action,
|
|
102
110
|
uuid
|
|
103
111
|
})
|
|
112
|
+
} else if (taskName === 'task:replay') {
|
|
113
|
+
const { path, cache } = args as { path: string, cache: string }
|
|
114
|
+
|
|
115
|
+
result = await task.run({
|
|
116
|
+
descriptorName: action,
|
|
117
|
+
path,
|
|
118
|
+
cache
|
|
119
|
+
})
|
|
104
120
|
} else if (taskName === 'task:run') {
|
|
105
121
|
result = await task.run({
|
|
106
122
|
descriptorName: action,
|
|
107
123
|
args
|
|
108
124
|
})
|
|
125
|
+
} else if (taskName === 'fixture:download') {
|
|
126
|
+
const { uuid } = args as { uuid: string }
|
|
127
|
+
|
|
128
|
+
result = await task.run({
|
|
129
|
+
descriptorName: action,
|
|
130
|
+
uuid
|
|
131
|
+
})
|
|
132
|
+
silent = true
|
|
109
133
|
} else if (taskName === 'auth:add') {
|
|
110
134
|
const { apiKey, apiSecret, url } = args as { name: string, apiKey: string, apiSecret: string, url: string }
|
|
111
135
|
|
|
@@ -124,6 +148,7 @@ runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
|
|
|
124
148
|
}
|
|
125
149
|
|
|
126
150
|
return {
|
|
151
|
+
silent,
|
|
127
152
|
outcome: 'Success',
|
|
128
153
|
taskName,
|
|
129
154
|
result
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// TASK: download
|
|
2
|
+
// Run this task with:
|
|
3
|
+
// forge task:run fixture:download --uuid [fixture-uuid]
|
|
4
|
+
|
|
5
|
+
import { createTask } from '@forgehive/task'
|
|
6
|
+
import { Schema } from '@forgehive/schema'
|
|
7
|
+
import axios from 'axios'
|
|
8
|
+
import path from 'path'
|
|
9
|
+
import fs from 'fs/promises'
|
|
10
|
+
import { loadCurrent as loadCurrentProfile } from '../auth/loadCurrent'
|
|
11
|
+
import { load as loadConf } from '../conf/load'
|
|
12
|
+
import { Profile } from '../types'
|
|
13
|
+
|
|
14
|
+
// Define the Fixture data structure
|
|
15
|
+
interface FixtureData {
|
|
16
|
+
name: string;
|
|
17
|
+
boundaries: Record<string, unknown>;
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface FixtureResponse {
|
|
22
|
+
fixture: FixtureData;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const description = 'Download a fixture by UUID to a path based on task descriptor returned from API'
|
|
27
|
+
|
|
28
|
+
const schema = new Schema({
|
|
29
|
+
uuid: Schema.string()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const boundaries = {
|
|
33
|
+
loadCurrentProfile: loadCurrentProfile.asBoundary(),
|
|
34
|
+
loadConf: loadConf.asBoundary(),
|
|
35
|
+
downloadFixture: async (uuid: string, profile: Profile): Promise<FixtureResponse> => {
|
|
36
|
+
const downloadUrl = `${profile.url}/api/fixture/${uuid}`
|
|
37
|
+
|
|
38
|
+
console.log(`Downloading fixture from ${downloadUrl}...`)
|
|
39
|
+
|
|
40
|
+
const authToken = `${profile.apiKey}:${profile.apiSecret}`
|
|
41
|
+
const response = await axios.get(downloadUrl, {
|
|
42
|
+
headers: {
|
|
43
|
+
Authorization: `Bearer ${authToken}`,
|
|
44
|
+
'Content-Type': 'application/json'
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return response.data
|
|
49
|
+
},
|
|
50
|
+
getCwd: async (): Promise<string> => {
|
|
51
|
+
return process.cwd()
|
|
52
|
+
},
|
|
53
|
+
persistFixture: async (filePath: string, data: FixtureData): Promise<{ path: string }> => {
|
|
54
|
+
const dirPath = path.dirname(filePath)
|
|
55
|
+
|
|
56
|
+
await fs.mkdir(dirPath, { recursive: true })
|
|
57
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8')
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
path: filePath
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const download = createTask(
|
|
66
|
+
schema,
|
|
67
|
+
boundaries,
|
|
68
|
+
async function ({ uuid }, {
|
|
69
|
+
downloadFixture,
|
|
70
|
+
getCwd,
|
|
71
|
+
persistFixture,
|
|
72
|
+
loadCurrentProfile,
|
|
73
|
+
loadConf
|
|
74
|
+
}) {
|
|
75
|
+
console.log('==================================================')
|
|
76
|
+
console.log(`Attempting to download fixture with uuid: ${uuid}`)
|
|
77
|
+
console.log('==================================================')
|
|
78
|
+
|
|
79
|
+
const profile = await loadCurrentProfile({})
|
|
80
|
+
const cwd = await getCwd()
|
|
81
|
+
const forge = await loadConf({})
|
|
82
|
+
|
|
83
|
+
// Download from hive api server first to get the task descriptor
|
|
84
|
+
let response
|
|
85
|
+
try {
|
|
86
|
+
response = await downloadFixture(uuid, profile)
|
|
87
|
+
} catch (e: unknown) {
|
|
88
|
+
const error = e as { status?: number, message: string }
|
|
89
|
+
console.error('Error downloading fixture:', error.message, error.status)
|
|
90
|
+
|
|
91
|
+
if (error.status === 404) {
|
|
92
|
+
throw new Error('Fixture not found')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
throw new Error('Failed to download fixture')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const fixture = response.fixture as FixtureData
|
|
99
|
+
const taskName = fixture.taskName as string
|
|
100
|
+
|
|
101
|
+
// Determine the output path using forge fixtures path and task descriptor
|
|
102
|
+
const fixturesBasePath = forge.paths.fixtures || 'fixtures'
|
|
103
|
+
const fixtureDir = path.join(fixturesBasePath, taskName)
|
|
104
|
+
const fixturePath = path.join(fixtureDir, `${uuid}.json`)
|
|
105
|
+
const filePath = path.resolve(cwd, fixturePath)
|
|
106
|
+
|
|
107
|
+
// Persist fixture to file
|
|
108
|
+
await persistFixture(filePath, response.fixture)
|
|
109
|
+
|
|
110
|
+
// Get the relative path for display in the replay command
|
|
111
|
+
const shortPath = path.join(taskName, `${uuid}.json`)
|
|
112
|
+
|
|
113
|
+
console.log(`
|
|
114
|
+
==================================================
|
|
115
|
+
Fixture download completed!
|
|
116
|
+
==================================================
|
|
117
|
+
Fixture UUID: ${uuid}
|
|
118
|
+
Task Name: ${taskName}
|
|
119
|
+
Saved to: ${filePath}
|
|
120
|
+
==================================================
|
|
121
|
+
Boundaries: ${Object.keys(fixture.boundaries).join(', ')}
|
|
122
|
+
==================================================
|
|
123
|
+
Replay with:
|
|
124
|
+
forge task:replay ${taskName} --path ${shortPath}
|
|
125
|
+
==================================================
|
|
126
|
+
`)
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
status: 'Downloaded',
|
|
130
|
+
path: filePath,
|
|
131
|
+
shortPath: shortPath
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
download.setDescription(description)
|
package/src/tasks/init.ts
CHANGED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// TASK: replay
|
|
2
|
+
// Run this task with:
|
|
3
|
+
// forge task:run task:replay
|
|
4
|
+
|
|
5
|
+
import { createTask } from '@forgehive/task'
|
|
6
|
+
import { Schema } from '@forgehive/schema'
|
|
7
|
+
import fs from 'fs/promises'
|
|
8
|
+
import path from 'path'
|
|
9
|
+
import os from 'os'
|
|
10
|
+
import axios from 'axios'
|
|
11
|
+
|
|
12
|
+
import { create as bundleCreate } from '../bundle/create'
|
|
13
|
+
import { load as bundleLoad } from '../bundle/load'
|
|
14
|
+
import { load as loadConf } from '../conf/load'
|
|
15
|
+
import { loadCurrent as loadCurrentProfile } from '../auth/loadCurrent'
|
|
16
|
+
import { type ForgeConf, type Profile } from '../types'
|
|
17
|
+
|
|
18
|
+
// Define the fixture structure type
|
|
19
|
+
interface Fixture {
|
|
20
|
+
fixtureUUID: string;
|
|
21
|
+
taskName: string;
|
|
22
|
+
projectName: string;
|
|
23
|
+
type: 'success' | 'error';
|
|
24
|
+
input: Record<string, unknown>;
|
|
25
|
+
output: Record<string, unknown>;
|
|
26
|
+
boundaries: Record<string, unknown>;
|
|
27
|
+
context: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const description = 'Replay a task execution from a specified path'
|
|
31
|
+
|
|
32
|
+
const schema = new Schema({
|
|
33
|
+
descriptorName: Schema.string(),
|
|
34
|
+
path: Schema.string(),
|
|
35
|
+
cache: Schema.string().optional()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const boundaries = {
|
|
39
|
+
readFixture: async (filePath: string): Promise<Fixture> => {
|
|
40
|
+
const fileContent = await fs.readFile(filePath, 'utf-8')
|
|
41
|
+
const parsedData = JSON.parse(fileContent) as Fixture
|
|
42
|
+
|
|
43
|
+
return parsedData
|
|
44
|
+
},
|
|
45
|
+
loadConf: loadConf.asBoundary(),
|
|
46
|
+
loadCurrentProfile: loadCurrentProfile.asBoundary(),
|
|
47
|
+
bundleCreate: bundleCreate.asBoundary(),
|
|
48
|
+
bundleLoad: bundleLoad.asBoundary(),
|
|
49
|
+
ensureBuildsFolder: async (): Promise<string> => {
|
|
50
|
+
const buildsPath = path.join(os.homedir(), '.forge', 'builds')
|
|
51
|
+
try {
|
|
52
|
+
await fs.access(buildsPath)
|
|
53
|
+
} catch {
|
|
54
|
+
await fs.mkdir(buildsPath, { recursive: true })
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return buildsPath
|
|
58
|
+
},
|
|
59
|
+
verifyLogFolder: async (logsPath: string): Promise<boolean> => {
|
|
60
|
+
// return true if the folder exists
|
|
61
|
+
try {
|
|
62
|
+
await fs.access(logsPath)
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return true
|
|
68
|
+
},
|
|
69
|
+
sendLogToAPI: async (profile: Profile, projectName: string, taskName: string, logItem: unknown, fixtureUUID: string): Promise<boolean> => {
|
|
70
|
+
try {
|
|
71
|
+
const logsUrl = `${profile.url}/api/tasks/log-ingest`
|
|
72
|
+
const authToken = `${profile.apiKey}:${profile.apiSecret}`
|
|
73
|
+
|
|
74
|
+
await axios.post(logsUrl, {
|
|
75
|
+
projectName,
|
|
76
|
+
taskName,
|
|
77
|
+
logItem: JSON.stringify(logItem),
|
|
78
|
+
replayFrom: fixtureUUID
|
|
79
|
+
}, {
|
|
80
|
+
headers: {
|
|
81
|
+
Authorization: `Bearer ${authToken}`,
|
|
82
|
+
'Content-Type': 'application/json'
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
console.log('===============================================')
|
|
87
|
+
console.log('Log sent to API... ', profile.name, profile.url)
|
|
88
|
+
console.log('Replay from fixture UUID:', fixtureUUID)
|
|
89
|
+
|
|
90
|
+
return true
|
|
91
|
+
} catch (e) {
|
|
92
|
+
const error = e as Error
|
|
93
|
+
console.error('Failed to send log to API:', error.message)
|
|
94
|
+
return false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const replay = createTask(
|
|
100
|
+
schema,
|
|
101
|
+
boundaries,
|
|
102
|
+
async function ({ descriptorName, path: fixturePath, cache }, { readFixture, loadConf, loadCurrentProfile, bundleCreate, bundleLoad, ensureBuildsFolder, verifyLogFolder, sendLogToAPI }) {
|
|
103
|
+
console.log('Input descriptorName:', descriptorName)
|
|
104
|
+
console.log('Input path:', fixturePath)
|
|
105
|
+
console.log('Input cache:', cache)
|
|
106
|
+
|
|
107
|
+
// Load forge configuration
|
|
108
|
+
const forge: ForgeConf = await loadConf({})
|
|
109
|
+
const taskDescriptor = forge.tasks[descriptorName as keyof typeof forge.tasks]
|
|
110
|
+
const projectName = forge.project.name
|
|
111
|
+
|
|
112
|
+
if (taskDescriptor === undefined) {
|
|
113
|
+
throw new Error(`Task ${descriptorName} is not defined in forge.json`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Resolve the fixture path (check if absolute, if not make it relative to logs folder)
|
|
117
|
+
const resolvedFixturePath = path.isAbsolute(fixturePath)
|
|
118
|
+
? fixturePath
|
|
119
|
+
: path.join(process.cwd(), forge.paths.fixtures, fixturePath)
|
|
120
|
+
|
|
121
|
+
// Read the file from the provided path
|
|
122
|
+
const fixture = await readFixture(resolvedFixturePath)
|
|
123
|
+
|
|
124
|
+
// Try to load profile, but continue if not found
|
|
125
|
+
let profile = null
|
|
126
|
+
try {
|
|
127
|
+
profile = await loadCurrentProfile({})
|
|
128
|
+
} catch (error) {
|
|
129
|
+
// Profile not found or not configured, continue without it
|
|
130
|
+
console.log('No profile found, logs will not be sent to remote API')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Verify if log folder exists
|
|
134
|
+
const logFolderPath = path.join(process.cwd(), forge.paths.logs)
|
|
135
|
+
const logFolderExists = await verifyLogFolder(logFolderPath)
|
|
136
|
+
if (!logFolderExists) {
|
|
137
|
+
throw new Error(`Log folder "${logFolderPath}" does not exist`)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Prepare paths
|
|
141
|
+
const entryPoint = path.join(process.cwd(), taskDescriptor.path)
|
|
142
|
+
const buildsPath = await ensureBuildsFolder()
|
|
143
|
+
const outputFile = path.join(buildsPath, `${descriptorName}.js`)
|
|
144
|
+
|
|
145
|
+
// Bundle the task
|
|
146
|
+
await bundleCreate({
|
|
147
|
+
entryPoint,
|
|
148
|
+
outputFile
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Load the bundled task
|
|
152
|
+
const bundle = await bundleLoad({
|
|
153
|
+
bundlePath: outputFile
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// Get the task handler
|
|
157
|
+
const task = bundle[taskDescriptor.handler]
|
|
158
|
+
|
|
159
|
+
if (!task) {
|
|
160
|
+
throw new Error(`Handler "${taskDescriptor.handler}" not found in bundle`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Configure boundaries based on cache parameter if provided
|
|
164
|
+
const boundaryConfig: Record<string, string> = {}
|
|
165
|
+
|
|
166
|
+
if (cache) {
|
|
167
|
+
// Parse the comma-separated list and trim each item
|
|
168
|
+
const cacheBoundaries = cache.split(',').map((b: string) => b.trim())
|
|
169
|
+
|
|
170
|
+
// Log which boundaries will use cache mode
|
|
171
|
+
if (cacheBoundaries.length > 0) {
|
|
172
|
+
// Set each specified boundary to 'replay' mode
|
|
173
|
+
cacheBoundaries.forEach((boundary: string) => {
|
|
174
|
+
boundaryConfig[boundary] = 'replay'
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log('==================================================')
|
|
180
|
+
console.log('UUID:', fixture.fixtureUUID)
|
|
181
|
+
console.log('Task name:', fixture.taskName)
|
|
182
|
+
console.log('Project name:', fixture.projectName)
|
|
183
|
+
console.log('Context:', fixture.context)
|
|
184
|
+
console.log('==================================================')
|
|
185
|
+
console.log('Replay:', fixture.input)
|
|
186
|
+
console.log('Boundaries:', JSON.stringify(fixture.boundaries, null, 2))
|
|
187
|
+
console.log('==================================================')
|
|
188
|
+
console.log('Boundary config:', boundaryConfig)
|
|
189
|
+
console.log('==================================================')
|
|
190
|
+
|
|
191
|
+
// Perform the replay
|
|
192
|
+
const [result, error, record] = await task.safeReplay(
|
|
193
|
+
{
|
|
194
|
+
input: fixture.input,
|
|
195
|
+
output: fixture.output,
|
|
196
|
+
boundaries: fixture.boundaries,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
boundaries: boundaryConfig // Use configured boundary modes
|
|
200
|
+
}
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
// Send the log to API if profile is available
|
|
204
|
+
if (profile) {
|
|
205
|
+
try {
|
|
206
|
+
await sendLogToAPI(profile, projectName, descriptorName, record, fixture.fixtureUUID)
|
|
207
|
+
} catch (e) {
|
|
208
|
+
console.error('Failed to send log to API:', e)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (error) {
|
|
213
|
+
throw new Error(error.message)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return result
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
replay.setDescription(description)
|