@forwardimpact/libutil 0.1.61 → 0.1.63
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/bin/fit-download-bundle.js +5 -2
- package/bin/fit-tiktoken.js +33 -0
- package/index.js +30 -19
- package/package.json +3 -2
- package/test/libutil.test.js +11 -14
- package/test/wait.test.js +33 -13
- package/wait.js +6 -3
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
3
4
|
import { createScriptConfig } from "@forwardimpact/libconfig";
|
|
4
5
|
import { createStorage } from "@forwardimpact/libstorage";
|
|
6
|
+
import { createLogger } from "@forwardimpact/libtelemetry";
|
|
5
7
|
import { createBundleDownloader, execLine } from "@forwardimpact/libutil";
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -11,11 +13,12 @@ import { createBundleDownloader, execLine } from "@forwardimpact/libutil";
|
|
|
11
13
|
*/
|
|
12
14
|
async function main() {
|
|
13
15
|
await createScriptConfig("download-bundle");
|
|
14
|
-
const
|
|
16
|
+
const logger = createLogger("generated");
|
|
17
|
+
const downloader = createBundleDownloader(createStorage, logger);
|
|
15
18
|
await downloader.download();
|
|
16
19
|
|
|
17
20
|
// If additional arguments provided, execute them after download
|
|
18
|
-
execLine();
|
|
21
|
+
execLine(0, { spawn, process });
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
main().catch((error) => {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { countTokens } from "@forwardimpact/libutil";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Counts tokens in the provided text
|
|
6
|
+
* Usage: fit-tiktoken <text>
|
|
7
|
+
* echo "text" | fit-tiktoken
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
10
|
+
async function main() {
|
|
11
|
+
let text = process.argv.slice(2).join(" ");
|
|
12
|
+
|
|
13
|
+
if (!text && !process.stdin.isTTY) {
|
|
14
|
+
const chunks = [];
|
|
15
|
+
for await (const chunk of process.stdin) {
|
|
16
|
+
chunks.push(chunk);
|
|
17
|
+
}
|
|
18
|
+
text = Buffer.concat(chunks).toString().trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!text) {
|
|
22
|
+
console.error("Usage: fit-tiktoken <text>");
|
|
23
|
+
console.error(' echo "text" | fit-tiktoken');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(countTokens(text));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
main().catch((error) => {
|
|
31
|
+
console.error(error.message);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
package/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { spawn } from "child_process";
|
|
5
4
|
|
|
6
5
|
import { Tokenizer, ranks } from "./tokenizer.js";
|
|
7
6
|
import { Finder } from "./finder.js";
|
|
@@ -10,16 +9,22 @@ import { TarExtractor } from "./extractor.js";
|
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* Updates or creates an environment variable in .env file
|
|
12
|
+
* @param {string} filePath - Path to .env file
|
|
13
13
|
* @param {string} key - Environment variable name (e.g., "SERVICE_SECRET")
|
|
14
14
|
* @param {string} value - Environment variable value
|
|
15
|
-
* @param {
|
|
15
|
+
* @param {object} fsFns - File system functions
|
|
16
|
+
* @param {Function} fsFns.readFile - Async file read function
|
|
17
|
+
* @param {Function} fsFns.writeFile - Async file write function
|
|
16
18
|
*/
|
|
17
|
-
export async function updateEnvFile(key, value,
|
|
18
|
-
|
|
19
|
+
export async function updateEnvFile(filePath, key, value, fsFns) {
|
|
20
|
+
if (!fsFns?.readFile) throw new Error("fsFns.readFile is required");
|
|
21
|
+
if (!fsFns?.writeFile) throw new Error("fsFns.writeFile is required");
|
|
22
|
+
|
|
23
|
+
const fullPath = path.resolve(filePath);
|
|
19
24
|
let content = "";
|
|
20
25
|
|
|
21
26
|
try {
|
|
22
|
-
content = await
|
|
27
|
+
content = await fsFns.readFile(fullPath, "utf8");
|
|
23
28
|
} catch (error) {
|
|
24
29
|
// It's ok if the file doesn't exist
|
|
25
30
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -47,7 +52,7 @@ export async function updateEnvFile(key, value, envPath = ".env") {
|
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
// Write back to file
|
|
50
|
-
await
|
|
55
|
+
await fsFns.writeFile(fullPath, lines.join("\n"));
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
/**
|
|
@@ -95,14 +100,13 @@ export function createTokenizer() {
|
|
|
95
100
|
* Creates a BundleDownloader instance configured for generated code management.
|
|
96
101
|
* Used in containerized deployments to download pre-generated code bundles.
|
|
97
102
|
* @param {Function} createStorage - Storage factory function from libstorage
|
|
98
|
-
* @
|
|
103
|
+
* @param {object} logger - Logger instance
|
|
104
|
+
* @returns {BundleDownloader} Configured BundleDownloader instance
|
|
99
105
|
*/
|
|
100
|
-
export
|
|
106
|
+
export function createBundleDownloader(createStorage, logger) {
|
|
101
107
|
if (!createStorage) throw new Error("createStorage is required");
|
|
108
|
+
if (!logger) throw new Error("logger is required");
|
|
102
109
|
|
|
103
|
-
// Dynamic import to avoid circular dependency with libtelemetry
|
|
104
|
-
const { createLogger } = await import("@forwardimpact/libtelemetry");
|
|
105
|
-
const logger = createLogger("generated");
|
|
106
110
|
const finder = new Finder(fs, logger);
|
|
107
111
|
const extractor = new TarExtractor(fs, path);
|
|
108
112
|
|
|
@@ -111,11 +115,18 @@ export async function createBundleDownloader(createStorage) {
|
|
|
111
115
|
|
|
112
116
|
/**
|
|
113
117
|
* Executes command line arguments as child process, similar to execv() in C
|
|
114
|
-
* @param {number}
|
|
118
|
+
* @param {number} shift - Number of arguments to skip from process.argv before extracting command
|
|
119
|
+
* @param {object} deps - Required dependencies
|
|
120
|
+
* @param {Function} deps.spawn - Child process spawn function
|
|
121
|
+
* @param {object} deps.process - Process object (argv, env, on, exit)
|
|
115
122
|
* @returns {void} Function does not return - exits parent process
|
|
116
123
|
*/
|
|
117
|
-
export function execLine(shift
|
|
118
|
-
|
|
124
|
+
export function execLine(shift, deps) {
|
|
125
|
+
if (!deps?.spawn) throw new Error("deps.spawn is required");
|
|
126
|
+
if (!deps?.process) throw new Error("deps.process is required");
|
|
127
|
+
|
|
128
|
+
const { spawn: spawnFn, process: proc } = deps;
|
|
129
|
+
const args = proc.argv.slice(2 + (shift || 0));
|
|
119
130
|
if (args.length === 0) return;
|
|
120
131
|
|
|
121
132
|
// Look for '--' delimiter and use everything after it as the command
|
|
@@ -125,23 +136,23 @@ export function execLine(shift = 0) {
|
|
|
125
136
|
if (line.length === 0) return;
|
|
126
137
|
|
|
127
138
|
const [command, ...commandArgs] = line;
|
|
128
|
-
const child =
|
|
139
|
+
const child = spawnFn(command, commandArgs, {
|
|
129
140
|
stdio: "inherit",
|
|
130
|
-
env:
|
|
141
|
+
env: proc.env,
|
|
131
142
|
});
|
|
132
143
|
|
|
133
144
|
// Forward signals to child process
|
|
134
145
|
["SIGTERM", "SIGINT", "SIGQUIT"].forEach((signal) => {
|
|
135
|
-
|
|
146
|
+
proc.on(signal, () => child.kill(signal));
|
|
136
147
|
});
|
|
137
148
|
|
|
138
149
|
child.on("error", (error) => {
|
|
139
150
|
console.error("Error:", error);
|
|
140
|
-
|
|
151
|
+
proc.exit(1);
|
|
141
152
|
});
|
|
142
153
|
|
|
143
154
|
child.on("exit", (code, signal) => {
|
|
144
|
-
|
|
155
|
+
proc.exit(signal ? 1 : code || 0);
|
|
145
156
|
});
|
|
146
157
|
}
|
|
147
158
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forwardimpact/libutil",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.63",
|
|
4
4
|
"description": "Utility functions and utilities for Guide",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "D. Olsson <hi@senzilla.io>",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "index.js",
|
|
9
9
|
"bin": {
|
|
10
|
-
"fit-download-bundle": "./bin/fit-download-bundle.js"
|
|
10
|
+
"fit-download-bundle": "./bin/fit-download-bundle.js",
|
|
11
|
+
"fit-tiktoken": "./bin/fit-tiktoken.js"
|
|
11
12
|
},
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=22.0.0"
|
package/test/libutil.test.js
CHANGED
|
@@ -4,35 +4,32 @@ import assert from "node:assert";
|
|
|
4
4
|
// Module under test
|
|
5
5
|
import { createBundleDownloader } from "../index.js";
|
|
6
6
|
|
|
7
|
+
const noop = () => {};
|
|
8
|
+
const mockLogger = { info: noop, debug: noop, warn: noop, error: noop };
|
|
9
|
+
|
|
7
10
|
describe("libutil", () => {
|
|
8
11
|
describe("createBundleDownloader", () => {
|
|
9
|
-
test("creates BundleDownloader instance with correct dependencies",
|
|
12
|
+
test("creates BundleDownloader instance with correct dependencies", () => {
|
|
10
13
|
const mockStorageFactory = mock.fn();
|
|
11
|
-
const mockProcess = { env: { STORAGE_TYPE: "local" } };
|
|
12
14
|
|
|
13
|
-
const downloader =
|
|
14
|
-
mockStorageFactory,
|
|
15
|
-
mockProcess,
|
|
16
|
-
);
|
|
15
|
+
const downloader = createBundleDownloader(mockStorageFactory, mockLogger);
|
|
17
16
|
|
|
18
17
|
assert.ok(downloader);
|
|
19
|
-
// Check that it's a BundleDownloader instance by checking if it has the expected methods
|
|
20
18
|
assert.ok(typeof downloader.initialize === "function");
|
|
21
19
|
assert.ok(typeof downloader.download === "function");
|
|
22
20
|
});
|
|
23
21
|
|
|
24
|
-
test("validates storageFactory parameter",
|
|
25
|
-
|
|
22
|
+
test("validates storageFactory parameter", () => {
|
|
23
|
+
assert.throws(() => createBundleDownloader(null, mockLogger), {
|
|
26
24
|
message: /createStorage is required/,
|
|
27
25
|
});
|
|
28
26
|
});
|
|
29
27
|
|
|
30
|
-
test("
|
|
28
|
+
test("validates logger parameter", () => {
|
|
31
29
|
const mockStorageFactory = mock.fn();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
assert.ok(downloader);
|
|
30
|
+
assert.throws(() => createBundleDownloader(mockStorageFactory, null), {
|
|
31
|
+
message: /logger is required/,
|
|
32
|
+
});
|
|
36
33
|
});
|
|
37
34
|
});
|
|
38
35
|
});
|
package/test/wait.test.js
CHANGED
|
@@ -3,13 +3,26 @@ import assert from "node:assert";
|
|
|
3
3
|
|
|
4
4
|
import { waitFor } from "../wait.js";
|
|
5
5
|
|
|
6
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
7
|
+
|
|
6
8
|
describe("waitFor", () => {
|
|
9
|
+
test("throws if delayFn is missing", async () => {
|
|
10
|
+
await assert.rejects(
|
|
11
|
+
() => waitFor(() => Promise.resolve(true), {}),
|
|
12
|
+
/delayFn is required/,
|
|
13
|
+
);
|
|
14
|
+
});
|
|
15
|
+
|
|
7
16
|
test("resolves immediately when condition is true", async () => {
|
|
8
17
|
let callCount = 0;
|
|
9
|
-
await waitFor(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
18
|
+
await waitFor(
|
|
19
|
+
() => {
|
|
20
|
+
callCount++;
|
|
21
|
+
return Promise.resolve(true);
|
|
22
|
+
},
|
|
23
|
+
{},
|
|
24
|
+
delay,
|
|
25
|
+
);
|
|
13
26
|
|
|
14
27
|
assert.strictEqual(callCount, 1);
|
|
15
28
|
});
|
|
@@ -22,6 +35,7 @@ describe("waitFor", () => {
|
|
|
22
35
|
return Promise.resolve(callCount >= 3);
|
|
23
36
|
},
|
|
24
37
|
{ interval: 10, timeout: 5000 },
|
|
38
|
+
delay,
|
|
25
39
|
);
|
|
26
40
|
|
|
27
41
|
assert.strictEqual(callCount, 3);
|
|
@@ -30,10 +44,11 @@ describe("waitFor", () => {
|
|
|
30
44
|
test("throws error on timeout", async () => {
|
|
31
45
|
await assert.rejects(
|
|
32
46
|
() =>
|
|
33
|
-
waitFor(
|
|
34
|
-
|
|
35
|
-
interval: 10,
|
|
36
|
-
|
|
47
|
+
waitFor(
|
|
48
|
+
() => Promise.resolve(false),
|
|
49
|
+
{ timeout: 50, interval: 10 },
|
|
50
|
+
delay,
|
|
51
|
+
),
|
|
37
52
|
{ message: "Timeout waiting for condition after 50ms" },
|
|
38
53
|
);
|
|
39
54
|
});
|
|
@@ -49,18 +64,22 @@ describe("waitFor", () => {
|
|
|
49
64
|
return Promise.resolve(true);
|
|
50
65
|
},
|
|
51
66
|
{ interval: 10, timeout: 5000 },
|
|
67
|
+
delay,
|
|
52
68
|
);
|
|
53
69
|
|
|
54
70
|
assert.strictEqual(callCount, 3);
|
|
55
71
|
});
|
|
56
72
|
|
|
57
73
|
test("uses default options when not provided", async () => {
|
|
58
|
-
// Just verify it doesn't throw with defaults
|
|
59
74
|
let called = false;
|
|
60
|
-
await waitFor(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
75
|
+
await waitFor(
|
|
76
|
+
() => {
|
|
77
|
+
called = true;
|
|
78
|
+
return Promise.resolve(true);
|
|
79
|
+
},
|
|
80
|
+
{},
|
|
81
|
+
delay,
|
|
82
|
+
);
|
|
64
83
|
|
|
65
84
|
assert.strictEqual(called, true);
|
|
66
85
|
});
|
|
@@ -81,6 +100,7 @@ describe("waitFor", () => {
|
|
|
81
100
|
return Promise.resolve(callCount >= 4);
|
|
82
101
|
},
|
|
83
102
|
{ interval: 20, maxInterval: 100, timeout: 5000 },
|
|
103
|
+
delay,
|
|
84
104
|
);
|
|
85
105
|
|
|
86
106
|
// Intervals should generally increase (with some tolerance for timing)
|
package/wait.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Poll until a condition returns true with exponential backoff
|
|
3
3
|
* @param {() => Promise<boolean>} checkFn - Function that returns true when ready
|
|
4
|
-
* @param {object}
|
|
4
|
+
* @param {object} options - Configuration options
|
|
5
5
|
* @param {number} [options.timeout] - Maximum time to wait in ms
|
|
6
6
|
* @param {number} [options.interval] - Initial polling interval in ms
|
|
7
7
|
* @param {number} [options.maxInterval] - Maximum polling interval in ms
|
|
8
|
+
* @param {(ms: number) => Promise<void>} delayFn - Function that returns a promise resolving after ms
|
|
8
9
|
* @returns {Promise<void>}
|
|
9
10
|
* @throws {Error} When timeout is reached
|
|
10
11
|
*/
|
|
11
|
-
export async function waitFor(checkFn, options
|
|
12
|
+
export async function waitFor(checkFn, options, delayFn) {
|
|
13
|
+
if (!delayFn) throw new Error("delayFn is required");
|
|
14
|
+
|
|
12
15
|
const { timeout = 30000, interval = 1000, maxInterval = 10000 } = options;
|
|
13
16
|
const startTime = Date.now();
|
|
14
17
|
let currentInterval = interval;
|
|
@@ -20,7 +23,7 @@ export async function waitFor(checkFn, options = {}) {
|
|
|
20
23
|
// Ignore errors during polling - service may not be up yet
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
await
|
|
26
|
+
await delayFn(currentInterval);
|
|
24
27
|
currentInterval = Math.min(currentInterval * 1.5, maxInterval);
|
|
25
28
|
}
|
|
26
29
|
|