@forgehive/forge-cli 0.2.10 → 0.2.12

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 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('Outcome', data);
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
@@ -57,9 +57,10 @@ runner.load('auth:remove', remove_3.remove);
57
57
  runner.setHandler(async (data) => {
58
58
  const parsedArgs = runner.parseArguments(data);
59
59
  const { taskName, action, args } = parsedArgs;
60
- console.log('========================================');
60
+ console.log('===============================================');
61
61
  console.log(`Running: ${taskName} ${action ? action : ''} ${JSON.stringify(args)}`);
62
- console.log('========================================');
62
+ console.log('===============================================');
63
+ let silent = false;
63
64
  const task = runner.getTask(taskName);
64
65
  if (!task) {
65
66
  throw new Error(`Task "${taskName}" not found`);
@@ -111,6 +112,7 @@ runner.setHandler(async (data) => {
111
112
  descriptorName: action,
112
113
  uuid
113
114
  });
115
+ silent = true;
114
116
  }
115
117
  else if (taskName === 'auth:add') {
116
118
  const { apiKey, apiSecret, url } = args;
@@ -130,6 +132,7 @@ runner.setHandler(async (data) => {
130
132
  result = await task.run(args);
131
133
  }
132
134
  return {
135
+ silent,
133
136
  outcome: 'Success',
134
137
  taskName,
135
138
  result
@@ -0,0 +1,26 @@
1
+ import archiver from 'archiver';
2
+ import fs from 'fs';
3
+ export declare const bytesToMB: (bytes: number) => string;
4
+ export declare const zip: import("@forgehive/task").TaskInstanceType<(argv: {
5
+ dir: string;
6
+ input: string;
7
+ output: string;
8
+ }, boundaries: import("@forgehive/task").WrappedBoundaries<{
9
+ createWriteStream: (outputPath: string) => Promise<fs.WriteStream>;
10
+ createArchiver: (format: "zip", options: {
11
+ zlib: {
12
+ level: number;
13
+ };
14
+ }) => Promise<archiver.Archiver>;
15
+ resolvePathDir: (dir: string, filename: string) => Promise<string>;
16
+ fileExists: (filePath: string) => Promise<boolean>;
17
+ }>) => Promise<unknown>, {
18
+ createWriteStream: (outputPath: string) => Promise<fs.WriteStream>;
19
+ createArchiver: (format: "zip", options: {
20
+ zlib: {
21
+ level: number;
22
+ };
23
+ }) => Promise<archiver.Archiver>;
24
+ resolvePathDir: (dir: string, filename: string) => Promise<string>;
25
+ fileExists: (filePath: string) => Promise<boolean>;
26
+ }>;
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ // TASK: zip
3
+ // Run this task with:
4
+ // forge task:run bundle:zip --dir .builds/ --input dailyUpdate.js --output dailyUpdate.zip
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.zip = exports.bytesToMB = void 0;
10
+ const task_1 = require("@forgehive/task");
11
+ const schema_1 = require("@forgehive/schema");
12
+ const archiver_1 = __importDefault(require("archiver"));
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const description = 'Zip a bundle file for distribution';
16
+ const schema = new schema_1.Schema({
17
+ dir: schema_1.Schema.string(),
18
+ input: schema_1.Schema.string(),
19
+ output: schema_1.Schema.string()
20
+ });
21
+ const boundaries = {
22
+ createWriteStream: async (outputPath) => {
23
+ return fs_1.default.createWriteStream(outputPath);
24
+ },
25
+ createArchiver: async (format, options) => {
26
+ return (0, archiver_1.default)(format, options);
27
+ },
28
+ resolvePathDir: async (dir, filename) => {
29
+ return path_1.default.resolve(dir, filename);
30
+ },
31
+ fileExists: async (filePath) => {
32
+ try {
33
+ await fs_1.default.promises.access(filePath);
34
+ return true;
35
+ }
36
+ catch {
37
+ return false;
38
+ }
39
+ }
40
+ };
41
+ const bytesToMB = (bytes) => {
42
+ const MB = bytes / (1024 * 1024);
43
+ return `${MB.toFixed(2)} MB`;
44
+ };
45
+ exports.bytesToMB = bytesToMB;
46
+ exports.zip = (0, task_1.createTask)(schema, boundaries, async function ({ dir, input, output }, { createWriteStream, createArchiver, resolvePathDir, fileExists }) {
47
+ const outputPath = await resolvePathDir(dir, output);
48
+ const inputPath = await resolvePathDir(dir, input);
49
+ const inputMapPath = inputPath + '.map';
50
+ // Check if input file exists
51
+ const inputExists = await fileExists(inputPath);
52
+ if (!inputExists) {
53
+ throw new Error(`Input file does not exist: ${inputPath}`);
54
+ }
55
+ // Check if source map exists before creating Promise
56
+ const mapExists = await fileExists(inputMapPath);
57
+ // Handle async operations outside of Promise constructor
58
+ const outStream = await createWriteStream(outputPath);
59
+ const archive = await createArchiver('zip', {
60
+ zlib: { level: 9 } // Sets the compression level
61
+ });
62
+ return new Promise((resolve, reject) => {
63
+ archive.on('error', function (err) {
64
+ reject(err);
65
+ });
66
+ outStream.on('end', function () {
67
+ console.log('Data has been drained');
68
+ });
69
+ outStream.on('close', function () {
70
+ setTimeout(() => {
71
+ resolve({
72
+ output,
73
+ outputPath,
74
+ size: archive.pointer()
75
+ });
76
+ }, 100);
77
+ });
78
+ archive.on('warning', function (err) {
79
+ if (err.code === 'ENOENT') {
80
+ console.warn('ENOENT', err);
81
+ }
82
+ else {
83
+ reject(err);
84
+ }
85
+ });
86
+ archive.pipe(outStream);
87
+ // Add the main bundle file
88
+ archive.file(inputPath, { name: 'index.js' });
89
+ // Add source map if it exists
90
+ if (mapExists) {
91
+ archive.file(inputMapPath, { name: 'index.js.map' });
92
+ }
93
+ archive.finalize();
94
+ });
95
+ });
96
+ exports.zip.setDescription(description);
@@ -1,6 +1,7 @@
1
1
  import { Profile } from '../types';
2
2
  interface FixtureData {
3
3
  name: string;
4
+ boundaries: Record<string, unknown>;
4
5
  [key: string]: unknown;
5
6
  }
6
7
  interface FixtureResponse {
@@ -17,10 +18,10 @@ export declare const download: import("@forgehive/task").TaskInstanceType<(argv:
17
18
  persistFixture: (filePath: string, data: FixtureData) => Promise<{
18
19
  path: string;
19
20
  }>;
20
- checkFileExists: (filePath: string) => Promise<boolean>;
21
21
  }>) => Promise<{
22
22
  status: string;
23
23
  path: string;
24
+ shortPath: string;
24
25
  }>, {
25
26
  loadCurrentProfile: (args: {}) => Promise<Promise<Profile>>;
26
27
  loadConf: (args: {}) => Promise<Promise<import("../types").ForgeConf>>;
@@ -29,6 +30,5 @@ export declare const download: import("@forgehive/task").TaskInstanceType<(argv:
29
30
  persistFixture: (filePath: string, data: FixtureData) => Promise<{
30
31
  path: string;
31
32
  }>;
32
- checkFileExists: (filePath: string) => Promise<boolean>;
33
33
  }>;
34
34
  export {};
@@ -43,20 +43,12 @@ const boundaries = {
43
43
  return {
44
44
  path: filePath
45
45
  };
46
- },
47
- checkFileExists: async (filePath) => {
48
- try {
49
- await promises_1.default.access(filePath);
50
- return true;
51
- }
52
- catch {
53
- return false;
54
- }
55
46
  }
56
47
  };
57
- exports.download = (0, task_1.createTask)(schema, boundaries, async function ({ uuid }, { downloadFixture, getCwd, persistFixture, checkFileExists, loadCurrentProfile, loadConf }) {
48
+ exports.download = (0, task_1.createTask)(schema, boundaries, async function ({ uuid }, { downloadFixture, getCwd, persistFixture, loadCurrentProfile, loadConf }) {
58
49
  console.log('==================================================');
59
50
  console.log(`Attempting to download fixture with uuid: ${uuid}`);
51
+ console.log('==================================================');
60
52
  const profile = await loadCurrentProfile({});
61
53
  const cwd = await getCwd();
62
54
  const forge = await loadConf({});
@@ -73,34 +65,35 @@ exports.download = (0, task_1.createTask)(schema, boundaries, async function ({
73
65
  }
74
66
  throw new Error('Failed to download fixture');
75
67
  }
76
- // Extract task descriptor from the response
77
- const taskName = response.fixture.name;
68
+ const fixture = response.fixture;
69
+ const taskName = fixture.taskName;
78
70
  // Determine the output path using forge fixtures path and task descriptor
79
71
  const fixturesBasePath = forge.paths.fixtures || 'fixtures';
80
72
  const fixtureDir = path_1.default.join(fixturesBasePath, taskName);
81
73
  const fixturePath = path_1.default.join(fixtureDir, `${uuid}.json`);
82
74
  const filePath = path_1.default.resolve(cwd, fixturePath);
83
- console.log(`Fixture will be saved to: ${filePath}`);
84
- // Check if file already exists
85
- const fileExists = await checkFileExists(filePath);
86
- if (fileExists) {
87
- console.log(`Fixture will be updated at ${filePath}`);
88
- }
89
- console.log(`
90
- ==================================================
91
- Starting fixture download!
92
- Fixture UUID: ${uuid}
93
- Task Name: ${taskName}
94
- Saving to: ${filePath}
95
- ==================================================
96
- Replay with: forge task:replay --path ${filePath}
97
- ==================================================
98
- `);
99
75
  // Persist fixture to file
100
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
+ `);
101
93
  return {
102
94
  status: 'Downloaded',
103
- path: filePath
95
+ path: filePath,
96
+ shortPath: shortPath
104
97
  };
105
98
  });
106
99
  exports.download.setDescription(description);
@@ -14,6 +14,11 @@ export declare const publish: import("@forgehive/task").TaskInstanceType<(argv:
14
14
  bundleLoad: (args: {
15
15
  bundlePath: string;
16
16
  }) => Promise<Promise<any>>;
17
+ bundleZip: (args: {
18
+ dir: string;
19
+ input: string;
20
+ output: string;
21
+ }) => Promise<Promise<unknown>>;
17
22
  readFileUtf8: (filePath: string) => Promise<string>;
18
23
  readFileBinary: (filePath: string) => Promise<Buffer>;
19
24
  publishTask: (data: any, profile: Profile) => Promise<any>;
@@ -35,6 +40,11 @@ export declare const publish: import("@forgehive/task").TaskInstanceType<(argv:
35
40
  bundleLoad: (args: {
36
41
  bundlePath: string;
37
42
  }) => Promise<Promise<any>>;
43
+ bundleZip: (args: {
44
+ dir: string;
45
+ input: string;
46
+ output: string;
47
+ }) => Promise<Promise<unknown>>;
38
48
  readFileUtf8: (filePath: string) => Promise<string>;
39
49
  readFileBinary: (filePath: string) => Promise<Buffer>;
40
50
  publishTask: (data: any, profile: Profile) => Promise<any>;
@@ -16,6 +16,7 @@ const os_1 = __importDefault(require("os"));
16
16
  const load_1 = require("../conf/load");
17
17
  const create_1 = require("../bundle/create");
18
18
  const load_2 = require("../bundle/load");
19
+ const zip_1 = require("../bundle/zip");
19
20
  const loadCurrent_1 = require("../auth/loadCurrent");
20
21
  const schema = new schema_1.Schema({
21
22
  descriptorName: schema_1.Schema.string()
@@ -28,6 +29,7 @@ const boundaries = {
28
29
  loadCurrentProfile: loadCurrent_1.loadCurrent.asBoundary(),
29
30
  bundleCreate: create_1.create.asBoundary(),
30
31
  bundleLoad: load_2.load.asBoundary(),
32
+ bundleZip: zip_1.zip.asBoundary(),
31
33
  readFileUtf8: async (filePath) => {
32
34
  return promises_1.default.readFile(filePath, 'utf-8');
33
35
  },
@@ -75,7 +77,7 @@ const boundaries = {
75
77
  return buildsPath;
76
78
  }
77
79
  };
78
- exports.publish = (0, task_1.createTask)(schema, boundaries, async function ({ descriptorName }, { getCwd, ensureBuildsFolder, loadConf, bundleCreate, bundleLoad, readFileUtf8, readFileBinary, publishTask, loadCurrentProfile, uploadBundleWithPresignedUrl }) {
80
+ exports.publish = (0, task_1.createTask)(schema, boundaries, async function ({ descriptorName }, { getCwd, ensureBuildsFolder, loadConf, bundleCreate, bundleLoad, bundleZip, readFileUtf8, readFileBinary, publishTask, loadCurrentProfile, uploadBundleWithPresignedUrl }) {
79
81
  const cwd = await getCwd();
80
82
  const forgeJson = await loadConf({});
81
83
  const profile = await loadCurrentProfile({});
@@ -87,12 +89,20 @@ exports.publish = (0, task_1.createTask)(schema, boundaries, async function ({ d
87
89
  const entryPoint = path_1.default.join(cwd, taskDescriptor.path);
88
90
  const buildsPath = await ensureBuildsFolder();
89
91
  const outputFile = path_1.default.join(buildsPath, `${descriptorName}.js`);
92
+ const zipFile = `${descriptorName}.zip`;
90
93
  // Bundle the task
91
94
  await bundleCreate({
92
95
  entryPoint,
93
96
  outputFile
94
97
  });
95
98
  console.log('Bundle created...');
99
+ // Zip the bundle
100
+ await bundleZip({
101
+ dir: buildsPath,
102
+ input: `${descriptorName}.js`,
103
+ output: zipFile
104
+ });
105
+ console.log('Bundle zipped...');
96
106
  // Load the bundled task
97
107
  const bundle = await bundleLoad({
98
108
  bundlePath: outputFile
@@ -105,13 +115,16 @@ exports.publish = (0, task_1.createTask)(schema, boundaries, async function ({ d
105
115
  const schemaDescriptor = schema.describe();
106
116
  // Read the task file content
107
117
  const sourceCode = await readFileUtf8(entryPoint);
108
- const bundleContent = await readFileBinary(outputFile);
118
+ // Read the zipped bundle instead of the raw bundle
119
+ const zipPath = path_1.default.join(buildsPath, zipFile);
120
+ const bundleContent = await readFileBinary(zipPath);
109
121
  // Get bundle size
110
122
  const bundleSize = bundleContent.length;
111
123
  // First, publish task metadata and get presigned URL for bundle upload
112
124
  const data = {
113
125
  ...taskDescriptor,
114
126
  taskName: descriptorName,
127
+ handler: taskDescriptor.handler,
115
128
  projectName,
116
129
  description,
117
130
  schemaDescriptor: JSON.stringify(schemaDescriptor),
@@ -122,9 +135,9 @@ exports.publish = (0, task_1.createTask)(schema, boundaries, async function ({ d
122
135
  // Publish metadata to hive api server
123
136
  console.log(`Publishing metadata and source code to ${profile.url}...`);
124
137
  const publishResponse = await publishTask(data, profile);
125
- // Upload bundle using the presigned URL
138
+ // Upload zipped bundle using the presigned URL
126
139
  if (publishResponse.bundleUploadUrl) {
127
- console.log('Uploading bundle...');
140
+ console.log('Uploading zipped bundle...');
128
141
  await uploadBundleWithPresignedUrl(publishResponse.bundleUploadUrl, bundleContent);
129
142
  return {
130
143
  descriptor: taskDescriptor,
@@ -1,7 +1,8 @@
1
1
  import { type ForgeConf, type Profile } from '../types';
2
2
  interface Fixture {
3
3
  fixtureUUID: string;
4
- name: string;
4
+ taskName: string;
5
+ projectName: string;
5
6
  type: 'success' | 'error';
6
7
  input: Record<string, unknown>;
7
8
  output: Record<string, unknown>;
@@ -10,6 +11,7 @@ interface Fixture {
10
11
  }
11
12
  export declare const replay: import("@forgehive/task").TaskInstanceType<(argv: {
12
13
  path: string;
14
+ descriptorName: string;
13
15
  cache?: string | undefined;
14
16
  }, boundaries: import("@forgehive/task").WrappedBoundaries<{
15
17
  readFixture: (filePath: string) => Promise<Fixture>;
@@ -19,6 +19,7 @@ const load_2 = require("../conf/load");
19
19
  const loadCurrent_1 = require("../auth/loadCurrent");
20
20
  const description = 'Replay a task execution from a specified path';
21
21
  const schema = new schema_1.Schema({
22
+ descriptorName: schema_1.Schema.string(),
22
23
  path: schema_1.Schema.string(),
23
24
  cache: schema_1.Schema.string().optional()
24
25
  });
@@ -79,18 +80,23 @@ const boundaries = {
79
80
  }
80
81
  }
81
82
  };
82
- exports.replay = (0, task_1.createTask)(schema, boundaries, async function (argv, { readFixture, loadConf, loadCurrentProfile, bundleCreate, bundleLoad, ensureBuildsFolder, verifyLogFolder, sendLogToAPI }) {
83
- console.log('Input path:', argv.path);
84
- // Read the file from the provided path
85
- const fixture = await readFixture(argv.path);
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);
86
87
  // Load forge configuration
87
88
  const forge = await loadConf({});
88
- const taskName = fixture.name;
89
- const taskDescriptor = forge.tasks[taskName];
89
+ const taskDescriptor = forge.tasks[descriptorName];
90
90
  const projectName = forge.project.name;
91
91
  if (taskDescriptor === undefined) {
92
- throw new Error(`Task ${taskName} is not defined in forge.json`);
92
+ throw new Error(`Task ${descriptorName} is not defined in forge.json`);
93
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);
94
100
  // Try to load profile, but continue if not found
95
101
  let profile = null;
96
102
  try {
@@ -109,7 +115,7 @@ exports.replay = (0, task_1.createTask)(schema, boundaries, async function (argv
109
115
  // Prepare paths
110
116
  const entryPoint = path_1.default.join(process.cwd(), taskDescriptor.path);
111
117
  const buildsPath = await ensureBuildsFolder();
112
- const outputFile = path_1.default.join(buildsPath, `${taskName}.js`);
118
+ const outputFile = path_1.default.join(buildsPath, `${descriptorName}.js`);
113
119
  // Bundle the task
114
120
  await bundleCreate({
115
121
  entryPoint,
@@ -126,20 +132,21 @@ exports.replay = (0, task_1.createTask)(schema, boundaries, async function (argv
126
132
  }
127
133
  // Configure boundaries based on cache parameter if provided
128
134
  const boundaryConfig = {};
129
- if (argv.cache) {
135
+ if (cache) {
130
136
  // Parse the comma-separated list and trim each item
131
- const cacheBoundaries = argv.cache.split(',').map(b => b.trim());
137
+ const cacheBoundaries = cache.split(',').map((b) => b.trim());
132
138
  // Log which boundaries will use cache mode
133
139
  if (cacheBoundaries.length > 0) {
134
140
  // Set each specified boundary to 'replay' mode
135
- cacheBoundaries.forEach(boundary => {
141
+ cacheBoundaries.forEach((boundary) => {
136
142
  boundaryConfig[boundary] = 'replay';
137
143
  });
138
144
  }
139
145
  }
140
146
  console.log('==================================================');
141
147
  console.log('UUID:', fixture.fixtureUUID);
142
- console.log('Name:', fixture.name);
148
+ console.log('Task name:', fixture.taskName);
149
+ console.log('Project name:', fixture.projectName);
143
150
  console.log('Context:', fixture.context);
144
151
  console.log('==================================================');
145
152
  console.log('Replay:', fixture.input);
@@ -158,7 +165,7 @@ exports.replay = (0, task_1.createTask)(schema, boundaries, async function (argv
158
165
  // Send the log to API if profile is available
159
166
  if (profile) {
160
167
  try {
161
- await sendLogToAPI(profile, projectName, taskName, record, fixture.fixtureUUID);
168
+ await sendLogToAPI(profile, projectName, descriptorName, record, fixture.fixtureUUID);
162
169
  }
163
170
  catch (e) {
164
171
  console.error('Failed to send log to API:', e);
package/forge.json CHANGED
@@ -85,6 +85,10 @@
85
85
  "fixture:download": {
86
86
  "path": "src/tasks/fixture/download.ts",
87
87
  "handler": "download"
88
+ },
89
+ "bundle:zip": {
90
+ "path": "src/tasks/bundle/zip.ts",
91
+ "handler": "zip"
88
92
  }
89
93
  },
90
94
  "runners": {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forgehive/forge-cli",
3
- "version": "0.2.10",
3
+ "version": "0.2.12",
4
4
  "description": "TypeScript CLI application",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -10,27 +10,29 @@
10
10
  "publishConfig": {
11
11
  "access": "public",
12
12
  "dependencies": {
13
- "@forgehive/record-tape": "^0.1.3",
14
- "@forgehive/runner": "^0.1.9",
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.9",
16
+ "@forgehive/task": "^0.1.12",
17
17
  "esbuild": "^0.25.0",
18
18
  "handlebars": "^4.7.8",
19
19
  "minimist": "^1.2.8"
20
20
  }
21
21
  },
22
22
  "dependencies": {
23
+ "archiver": "^7.0.1",
23
24
  "axios": "^1.8.4",
24
25
  "dotenv": "^16.5.0",
25
26
  "esbuild": "^0.25.0",
26
27
  "handlebars": "^4.7.8",
27
28
  "minimist": "^1.2.8",
28
- "@forgehive/runner": "0.1.9",
29
- "@forgehive/schema": "0.1.4",
30
- "@forgehive/task": "0.1.9",
31
- "@forgehive/record-tape": "0.1.3"
29
+ "@forgehive/runner": "0.1.11",
30
+ "@forgehive/record-tape": "0.1.5",
31
+ "@forgehive/task": "0.1.12",
32
+ "@forgehive/schema": "0.1.4"
32
33
  },
33
34
  "devDependencies": {
35
+ "@types/archiver": "^6.0.3",
34
36
  "@types/jest": "^29.5.3",
35
37
  "@types/minimist": "^1.2.5",
36
38
  "@types/node": "^20.4.5",
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
- runner.handler(args).then(data => {
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('Outcome', data)
24
+ console.log('Result', result)
11
25
  console.log('===============================================')
12
26
  }).catch((e) => {
13
27
  console.error(e)
package/src/runner.ts CHANGED
@@ -72,10 +72,11 @@ runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
72
72
  const parsedArgs = runner.parseArguments(data)
73
73
  const { taskName, action, args } = parsedArgs
74
74
 
75
- console.log('========================================')
75
+ console.log('===============================================')
76
76
  console.log(`Running: ${taskName} ${action ? action : ''} ${JSON.stringify(args)}`)
77
- console.log('========================================')
77
+ console.log('===============================================')
78
78
 
79
+ let silent = false
79
80
  const task = runner.getTask(taskName)
80
81
  if (!task) {
81
82
  throw new Error(`Task "${taskName}" not found`)
@@ -128,6 +129,7 @@ runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
128
129
  descriptorName: action,
129
130
  uuid
130
131
  })
132
+ silent = true
131
133
  } else if (taskName === 'auth:add') {
132
134
  const { apiKey, apiSecret, url } = args as { name: string, apiKey: string, apiSecret: string, url: string }
133
135
 
@@ -146,6 +148,7 @@ runner.setHandler(async (data: ParsedArgs): Promise<unknown> => {
146
148
  }
147
149
 
148
150
  return {
151
+ silent,
149
152
  outcome: 'Success',
150
153
  taskName,
151
154
  result
@@ -0,0 +1,109 @@
1
+ // TASK: zip
2
+ // Run this task with:
3
+ // forge task:run bundle:zip --dir .builds/ --input dailyUpdate.js --output dailyUpdate.zip
4
+
5
+ import { createTask } from '@forgehive/task'
6
+ import { Schema } from '@forgehive/schema'
7
+ import archiver from 'archiver'
8
+ import fs from 'fs'
9
+ import path from 'path'
10
+
11
+ const description = 'Zip a bundle file for distribution'
12
+
13
+ const schema = new Schema({
14
+ dir: Schema.string(),
15
+ input: Schema.string(),
16
+ output: Schema.string()
17
+ })
18
+
19
+ const boundaries = {
20
+ createWriteStream: async (outputPath: string): Promise<fs.WriteStream> => {
21
+ return fs.createWriteStream(outputPath)
22
+ },
23
+ createArchiver: async (format: 'zip', options: { zlib: { level: number } }): Promise<archiver.Archiver> => {
24
+ return archiver(format, options)
25
+ },
26
+ resolvePathDir: async (dir: string, filename: string): Promise<string> => {
27
+ return path.resolve(dir, filename)
28
+ },
29
+ fileExists: async (filePath: string): Promise<boolean> => {
30
+ try {
31
+ await fs.promises.access(filePath)
32
+ return true
33
+ } catch {
34
+ return false
35
+ }
36
+ }
37
+ }
38
+
39
+ export const bytesToMB = (bytes: number): string => {
40
+ const MB = bytes / (1024 * 1024)
41
+ return `${MB.toFixed(2)} MB`
42
+ }
43
+
44
+ export const zip = createTask(
45
+ schema,
46
+ boundaries,
47
+ async function ({ dir, input, output }, { createWriteStream, createArchiver, resolvePathDir, fileExists }) {
48
+ const outputPath = await resolvePathDir(dir, output)
49
+ const inputPath = await resolvePathDir(dir, input)
50
+ const inputMapPath = inputPath + '.map'
51
+
52
+ // Check if input file exists
53
+ const inputExists = await fileExists(inputPath)
54
+ if (!inputExists) {
55
+ throw new Error(`Input file does not exist: ${inputPath}`)
56
+ }
57
+
58
+ // Check if source map exists before creating Promise
59
+ const mapExists = await fileExists(inputMapPath)
60
+
61
+ // Handle async operations outside of Promise constructor
62
+ const outStream = await createWriteStream(outputPath)
63
+ const archive = await createArchiver('zip', {
64
+ zlib: { level: 9 } // Sets the compression level
65
+ })
66
+
67
+ return new Promise((resolve, reject) => {
68
+ archive.on('error', function (err: Error) {
69
+ reject(err)
70
+ })
71
+
72
+ outStream.on('end', function () {
73
+ console.log('Data has been drained')
74
+ })
75
+
76
+ outStream.on('close', function () {
77
+ setTimeout(() => {
78
+ resolve({
79
+ output,
80
+ outputPath,
81
+ size: archive.pointer()
82
+ })
83
+ }, 100)
84
+ })
85
+
86
+ archive.on('warning', function (err: archiver.ArchiverError) {
87
+ if (err.code === 'ENOENT') {
88
+ console.warn('ENOENT', err)
89
+ } else {
90
+ reject(err)
91
+ }
92
+ })
93
+
94
+ archive.pipe(outStream)
95
+
96
+ // Add the main bundle file
97
+ archive.file(inputPath, { name: 'index.js' })
98
+
99
+ // Add source map if it exists
100
+ if (mapExists) {
101
+ archive.file(inputMapPath, { name: 'index.js.map' })
102
+ }
103
+
104
+ archive.finalize()
105
+ })
106
+ }
107
+ )
108
+
109
+ zip.setDescription(description)
@@ -14,6 +14,7 @@ import { Profile } from '../types'
14
14
  // Define the Fixture data structure
15
15
  interface FixtureData {
16
16
  name: string;
17
+ boundaries: Record<string, unknown>;
17
18
  [key: string]: unknown;
18
19
  }
19
20
 
@@ -58,14 +59,6 @@ const boundaries = {
58
59
  return {
59
60
  path: filePath
60
61
  }
61
- },
62
- checkFileExists: async (filePath: string): Promise<boolean> => {
63
- try {
64
- await fs.access(filePath)
65
- return true
66
- } catch {
67
- return false
68
- }
69
62
  }
70
63
  }
71
64
 
@@ -76,12 +69,12 @@ export const download = createTask(
76
69
  downloadFixture,
77
70
  getCwd,
78
71
  persistFixture,
79
- checkFileExists,
80
72
  loadCurrentProfile,
81
73
  loadConf
82
74
  }) {
83
75
  console.log('==================================================')
84
76
  console.log(`Attempting to download fixture with uuid: ${uuid}`)
77
+ console.log('==================================================')
85
78
 
86
79
  const profile = await loadCurrentProfile({})
87
80
  const cwd = await getCwd()
@@ -102,8 +95,8 @@ export const download = createTask(
102
95
  throw new Error('Failed to download fixture')
103
96
  }
104
97
 
105
- // Extract task descriptor from the response
106
- const taskName = response.fixture.name
98
+ const fixture = response.fixture as FixtureData
99
+ const taskName = fixture.taskName as string
107
100
 
108
101
  // Determine the output path using forge fixtures path and task descriptor
109
102
  const fixturesBasePath = forge.paths.fixtures || 'fixtures'
@@ -111,31 +104,31 @@ export const download = createTask(
111
104
  const fixturePath = path.join(fixtureDir, `${uuid}.json`)
112
105
  const filePath = path.resolve(cwd, fixturePath)
113
106
 
114
- console.log(`Fixture will be saved to: ${filePath}`)
107
+ // Persist fixture to file
108
+ await persistFixture(filePath, response.fixture)
115
109
 
116
- // Check if file already exists
117
- const fileExists = await checkFileExists(filePath)
118
- if (fileExists) {
119
- console.log(`Fixture will be updated at ${filePath}`)
120
- }
110
+ // Get the relative path for display in the replay command
111
+ const shortPath = path.join(taskName, `${uuid}.json`)
121
112
 
122
113
  console.log(`
123
- ==================================================
124
- Starting fixture download!
125
- Fixture UUID: ${uuid}
126
- Task Name: ${taskName}
127
- Saving to: ${filePath}
128
- ==================================================
129
- Replay with: forge task:replay --path ${filePath}
130
- ==================================================
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
+ ==================================================
131
126
  `)
132
127
 
133
- // Persist fixture to file
134
- await persistFixture(filePath, response.fixture)
135
-
136
128
  return {
137
129
  status: 'Downloaded',
138
- path: filePath
130
+ path: filePath,
131
+ shortPath: shortPath
139
132
  }
140
133
  }
141
134
  )
@@ -13,6 +13,7 @@ import os from 'os'
13
13
  import { load as loadConf } from '../conf/load'
14
14
  import { create as bundleCreate } from '../bundle/create'
15
15
  import { load as bundleLoad } from '../bundle/load'
16
+ import { zip as bundleZip } from '../bundle/zip'
16
17
  import { loadCurrent as loadCurrentProfile } from '../auth/loadCurrent'
17
18
  import { Profile } from '../types'
18
19
 
@@ -28,6 +29,7 @@ const boundaries = {
28
29
  loadCurrentProfile: loadCurrentProfile.asBoundary(),
29
30
  bundleCreate: bundleCreate.asBoundary(),
30
31
  bundleLoad: bundleLoad.asBoundary(),
32
+ bundleZip: bundleZip.asBoundary(),
31
33
  readFileUtf8: async (filePath: string): Promise<string> => {
32
34
  return fs.readFile(filePath, 'utf-8')
33
35
  },
@@ -88,6 +90,7 @@ export const publish = createTask(
88
90
  loadConf,
89
91
  bundleCreate,
90
92
  bundleLoad,
93
+ bundleZip,
91
94
  readFileUtf8,
92
95
  readFileBinary,
93
96
  publishTask,
@@ -108,6 +111,7 @@ export const publish = createTask(
108
111
  const entryPoint = path.join(cwd, taskDescriptor.path)
109
112
  const buildsPath = await ensureBuildsFolder()
110
113
  const outputFile = path.join(buildsPath, `${descriptorName}.js`)
114
+ const zipFile = `${descriptorName}.zip`
111
115
 
112
116
  // Bundle the task
113
117
  await bundleCreate({
@@ -117,6 +121,15 @@ export const publish = createTask(
117
121
 
118
122
  console.log('Bundle created...')
119
123
 
124
+ // Zip the bundle
125
+ await bundleZip({
126
+ dir: buildsPath,
127
+ input: `${descriptorName}.js`,
128
+ output: zipFile
129
+ })
130
+
131
+ console.log('Bundle zipped...')
132
+
120
133
  // Load the bundled task
121
134
  const bundle = await bundleLoad({
122
135
  bundlePath: outputFile
@@ -131,7 +144,9 @@ export const publish = createTask(
131
144
 
132
145
  // Read the task file content
133
146
  const sourceCode = await readFileUtf8(entryPoint)
134
- const bundleContent = await readFileBinary(outputFile)
147
+ // Read the zipped bundle instead of the raw bundle
148
+ const zipPath = path.join(buildsPath, zipFile)
149
+ const bundleContent = await readFileBinary(zipPath)
135
150
 
136
151
  // Get bundle size
137
152
  const bundleSize = bundleContent.length
@@ -140,6 +155,7 @@ export const publish = createTask(
140
155
  const data = {
141
156
  ...taskDescriptor,
142
157
  taskName: descriptorName,
158
+ handler: taskDescriptor.handler,
143
159
  projectName,
144
160
  description,
145
161
  schemaDescriptor: JSON.stringify(schemaDescriptor),
@@ -152,9 +168,9 @@ export const publish = createTask(
152
168
  console.log(`Publishing metadata and source code to ${profile.url}...`)
153
169
  const publishResponse = await publishTask(data, profile)
154
170
 
155
- // Upload bundle using the presigned URL
171
+ // Upload zipped bundle using the presigned URL
156
172
  if (publishResponse.bundleUploadUrl) {
157
- console.log('Uploading bundle...')
173
+ console.log('Uploading zipped bundle...')
158
174
  await uploadBundleWithPresignedUrl(
159
175
  publishResponse.bundleUploadUrl,
160
176
  bundleContent
@@ -18,7 +18,8 @@ import { type ForgeConf, type Profile } from '../types'
18
18
  // Define the fixture structure type
19
19
  interface Fixture {
20
20
  fixtureUUID: string;
21
- name: string;
21
+ taskName: string;
22
+ projectName: string;
22
23
  type: 'success' | 'error';
23
24
  input: Record<string, unknown>;
24
25
  output: Record<string, unknown>;
@@ -29,6 +30,7 @@ interface Fixture {
29
30
  const description = 'Replay a task execution from a specified path'
30
31
 
31
32
  const schema = new Schema({
33
+ descriptorName: Schema.string(),
32
34
  path: Schema.string(),
33
35
  cache: Schema.string().optional()
34
36
  })
@@ -97,22 +99,28 @@ const boundaries = {
97
99
  export const replay = createTask(
98
100
  schema,
99
101
  boundaries,
100
- async function (argv, { readFixture, loadConf, loadCurrentProfile, bundleCreate, bundleLoad, ensureBuildsFolder, verifyLogFolder, sendLogToAPI }) {
101
- console.log('Input path:', argv.path)
102
-
103
- // Read the file from the provided path
104
- const fixture = await readFixture(argv.path)
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)
105
106
 
106
107
  // Load forge configuration
107
108
  const forge: ForgeConf = await loadConf({})
108
- const taskName = fixture.name
109
- const taskDescriptor = forge.tasks[taskName as keyof typeof forge.tasks]
109
+ const taskDescriptor = forge.tasks[descriptorName as keyof typeof forge.tasks]
110
110
  const projectName = forge.project.name
111
111
 
112
112
  if (taskDescriptor === undefined) {
113
- throw new Error(`Task ${taskName} is not defined in forge.json`)
113
+ throw new Error(`Task ${descriptorName} is not defined in forge.json`)
114
114
  }
115
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
+
116
124
  // Try to load profile, but continue if not found
117
125
  let profile = null
118
126
  try {
@@ -132,7 +140,7 @@ export const replay = createTask(
132
140
  // Prepare paths
133
141
  const entryPoint = path.join(process.cwd(), taskDescriptor.path)
134
142
  const buildsPath = await ensureBuildsFolder()
135
- const outputFile = path.join(buildsPath, `${taskName}.js`)
143
+ const outputFile = path.join(buildsPath, `${descriptorName}.js`)
136
144
 
137
145
  // Bundle the task
138
146
  await bundleCreate({
@@ -155,14 +163,14 @@ export const replay = createTask(
155
163
  // Configure boundaries based on cache parameter if provided
156
164
  const boundaryConfig: Record<string, string> = {}
157
165
 
158
- if (argv.cache) {
166
+ if (cache) {
159
167
  // Parse the comma-separated list and trim each item
160
- const cacheBoundaries = argv.cache.split(',').map(b => b.trim())
168
+ const cacheBoundaries = cache.split(',').map((b: string) => b.trim())
161
169
 
162
170
  // Log which boundaries will use cache mode
163
171
  if (cacheBoundaries.length > 0) {
164
172
  // Set each specified boundary to 'replay' mode
165
- cacheBoundaries.forEach(boundary => {
173
+ cacheBoundaries.forEach((boundary: string) => {
166
174
  boundaryConfig[boundary] = 'replay'
167
175
  })
168
176
  }
@@ -170,7 +178,8 @@ export const replay = createTask(
170
178
 
171
179
  console.log('==================================================')
172
180
  console.log('UUID:', fixture.fixtureUUID)
173
- console.log('Name:', fixture.name)
181
+ console.log('Task name:', fixture.taskName)
182
+ console.log('Project name:', fixture.projectName)
174
183
  console.log('Context:', fixture.context)
175
184
  console.log('==================================================')
176
185
  console.log('Replay:', fixture.input)
@@ -194,7 +203,7 @@ export const replay = createTask(
194
203
  // Send the log to API if profile is available
195
204
  if (profile) {
196
205
  try {
197
- await sendLogToAPI(profile, projectName, taskName, record, fixture.fixtureUUID)
206
+ await sendLogToAPI(profile, projectName, descriptorName, record, fixture.fixtureUUID)
198
207
  } catch (e) {
199
208
  console.error('Failed to send log to API:', e)
200
209
  }