@livestore/utils-dev 0.0.0-snapshot-c0f03c6e0f72a2bf15dde9a0b7d25352f79da9aa → 0.0.0-snapshot-ae29f95eb90e37e6b0ae25179f2fc8e6f6c238cd
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/.tsbuildinfo.json +1 -1
- package/dist/node/cmd-log.d.ts +21 -0
- package/dist/node/cmd-log.d.ts.map +1 -0
- package/dist/node/cmd-log.js +56 -0
- package/dist/node/cmd-log.js.map +1 -0
- package/dist/node/cmd.d.ts +9 -0
- package/dist/node/cmd.d.ts.map +1 -1
- package/dist/node/cmd.js +38 -8
- package/dist/node/cmd.js.map +1 -1
- package/dist/node/cmd.test.d.ts +2 -0
- package/dist/node/cmd.test.d.ts.map +1 -0
- package/dist/node/cmd.test.js +48 -0
- package/dist/node/cmd.test.js.map +1 -0
- package/package.json +2 -2
- package/src/node/cmd-log.ts +100 -0
- package/src/node/cmd.test.ts +61 -0
- package/src/node/cmd.ts +53 -8
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Effect } from '@livestore/utils/effect';
|
|
2
|
+
export type TCmdLoggingOptions = {
|
|
3
|
+
readonly logDir?: string;
|
|
4
|
+
readonly logFileName?: string;
|
|
5
|
+
readonly logRetention?: number;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Prepares logging directories, archives previous canonical log and prunes archives.
|
|
9
|
+
* Returns the canonical current log path if logging is enabled, otherwise undefined.
|
|
10
|
+
*/
|
|
11
|
+
export declare const prepareCmdLogging: (options: TCmdLoggingOptions) => Effect.Effect<string | undefined, never, never>;
|
|
12
|
+
/**
|
|
13
|
+
* Given a command input, applies logging by piping output through `tee` to the
|
|
14
|
+
* canonical log file. Returns the transformed input and whether a shell is required.
|
|
15
|
+
*/
|
|
16
|
+
export declare const applyLoggingToCommand: (commandInput: string | (string | undefined)[], options: TCmdLoggingOptions) => Effect.Effect<{
|
|
17
|
+
input: string | string[];
|
|
18
|
+
subshell: boolean;
|
|
19
|
+
logPath?: string;
|
|
20
|
+
}, never, never>;
|
|
21
|
+
//# sourceMappingURL=cmd-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cmd-log.d.ts","sourceRoot":"","sources":["../../src/node/cmd-log.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAY,MAAM,yBAAyB,CAAA;AAE1D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAC/B,CAAA;AAID;;;GAGG;AACH,eAAO,MAAM,iBAAiB,EAAE,CAAC,OAAO,EAAE,kBAAkB,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,CAqD3G,CAAA;AAEJ;;;GAGG;AACH,eAAO,MAAM,qBAAqB,EAAE,CAClC,YAAY,EAAE,MAAM,GAAG,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,EAC7C,OAAO,EAAE,kBAAkB,KACxB,MAAM,CAAC,MAAM,CAAC;IAAE,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,KAAK,EAAE,KAAK,CAmBhG,CAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { isNotUndefined } from '@livestore/utils';
|
|
4
|
+
import { Effect, identity } from '@livestore/utils/effect';
|
|
5
|
+
const shellEscape = (s) => (/[A-Za-z0-9_./:=+@-]+/.test(s) ? s : `'${s.replaceAll("'", `'"'"'`)}'`);
|
|
6
|
+
/**
|
|
7
|
+
* Prepares logging directories, archives previous canonical log and prunes archives.
|
|
8
|
+
* Returns the canonical current log path if logging is enabled, otherwise undefined.
|
|
9
|
+
*/
|
|
10
|
+
export const prepareCmdLogging = Effect.fn('cmd.logging.prepare')(function* ({ logDir, logFileName = 'dev.log', logRetention = 50, }) {
|
|
11
|
+
if (!logDir || logDir === '')
|
|
12
|
+
return undefined;
|
|
13
|
+
const logsDir = logDir;
|
|
14
|
+
const archiveDir = path.join(logsDir, 'archive');
|
|
15
|
+
const currentLogPath = path.join(logsDir, logFileName);
|
|
16
|
+
// Ensure directories exist
|
|
17
|
+
yield* Effect.sync(() => fs.mkdirSync(archiveDir, { recursive: true }));
|
|
18
|
+
// Archive previous log if present
|
|
19
|
+
if (fs.existsSync(currentLogPath)) {
|
|
20
|
+
const safeIso = new Date().toISOString().replaceAll(':', '-');
|
|
21
|
+
const archivedBase = `${path.parse(logFileName).name}-${safeIso}.log`;
|
|
22
|
+
const archivedLog = path.join(archiveDir, archivedBase);
|
|
23
|
+
yield* Effect.try({ try: () => fs.renameSync(currentLogPath, archivedLog), catch: identity }).pipe(Effect.catchAll(() => Effect.try({
|
|
24
|
+
try: () => {
|
|
25
|
+
fs.copyFileSync(currentLogPath, archivedLog);
|
|
26
|
+
fs.truncateSync(currentLogPath, 0);
|
|
27
|
+
},
|
|
28
|
+
catch: identity,
|
|
29
|
+
})), Effect.ignore);
|
|
30
|
+
// Prune archives to retain only the newest N
|
|
31
|
+
yield* Effect.try({ try: () => fs.readdirSync(archiveDir), catch: identity }).pipe(Effect.map((names) => names.filter((n) => n.endsWith('.log'))), Effect.map((names) => names
|
|
32
|
+
.map((name) => ({ name, mtimeMs: fs.statSync(path.join(archiveDir, name)).mtimeMs }))
|
|
33
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)), Effect.flatMap((entries) => Effect.forEach(entries.slice(logRetention), (e) => Effect.try({ try: () => fs.unlinkSync(path.join(archiveDir, e.name)), catch: identity }).pipe(Effect.ignore))), Effect.ignore);
|
|
34
|
+
}
|
|
35
|
+
return currentLogPath;
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Given a command input, applies logging by piping output through `tee` to the
|
|
39
|
+
* canonical log file. Returns the transformed input and whether a shell is required.
|
|
40
|
+
*/
|
|
41
|
+
export const applyLoggingToCommand = Effect.fn('cmd.logging.apply')(function* (commandInput, options) {
|
|
42
|
+
const asArray = Array.isArray(commandInput);
|
|
43
|
+
const parts = asArray ? commandInput.filter(isNotUndefined) : undefined;
|
|
44
|
+
const logPath = yield* prepareCmdLogging(options);
|
|
45
|
+
if (!logPath) {
|
|
46
|
+
return {
|
|
47
|
+
input: asArray ? (parts ?? []) : commandInput,
|
|
48
|
+
subshell: false,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const input = asArray
|
|
52
|
+
? [...(parts ?? []), '2>&1', '|', 'tee', logPath]
|
|
53
|
+
: `${commandInput} 2>&1 | tee ${shellEscape(logPath)}`;
|
|
54
|
+
return { input, subshell: true, logPath };
|
|
55
|
+
});
|
|
56
|
+
//# sourceMappingURL=cmd-log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cmd-log.js","sourceRoot":"","sources":["../../src/node/cmd-log.ts"],"names":[],"mappings":"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"}
|
package/dist/node/cmd.d.ts
CHANGED
|
@@ -5,6 +5,15 @@ export declare const cmd: (commandInput: string | (string | undefined)[], option
|
|
|
5
5
|
stdout?: 'inherit' | 'pipe';
|
|
6
6
|
shell?: boolean;
|
|
7
7
|
env?: Record<string, string | undefined>;
|
|
8
|
+
/**
|
|
9
|
+
* When provided, streams command output to terminal AND to a canonical log file (`${logDir}/dev.log`) in this directory.
|
|
10
|
+
* Also archives the previous run to `${logDir}/archive/dev-<ISO>.log` and keeps only the latest 50 archives.
|
|
11
|
+
*/
|
|
12
|
+
logDir?: string;
|
|
13
|
+
/** Optional basename for the canonical log file; defaults to 'dev.log' */
|
|
14
|
+
logFileName?: string;
|
|
15
|
+
/** Optional number of archived logs to retain; defaults to 50 */
|
|
16
|
+
logRetention?: number;
|
|
8
17
|
} | undefined) => Effect.Effect<CommandExecutor.ExitCode, PlatformError.PlatformError | CmdError, CommandExecutor.CommandExecutor>;
|
|
9
18
|
export declare const cmdText: (commandInput: string | (string | undefined)[], options?: {
|
|
10
19
|
cwd?: string;
|
package/dist/node/cmd.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cmd.d.ts","sourceRoot":"","sources":["../../src/node/cmd.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,KAAK,eAAe,EAAE,MAAM,EAAY,KAAK,aAAa,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"cmd.d.ts","sourceRoot":"","sources":["../../src/node/cmd.ts"],"names":[],"mappings":"AACA,OAAO,EAAW,KAAK,eAAe,EAAE,MAAM,EAAY,KAAK,aAAa,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAA;AAIrH,eAAO,MAAM,GAAG,EAAE,CAChB,YAAY,EAAE,MAAM,GAAG,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,EAC7C,OAAO,CAAC,EACJ;IACE,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;IAC3B,MAAM,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IACxC;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0EAA0E;IAC1E,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,iEAAiE;IACjE,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,GACD,SAAS,KACV,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,GAAG,QAAQ,EAAE,eAAe,CAAC,eAAe,CA2EhH,CAAA;AAEJ,eAAO,MAAM,OAAO,EAAE,CACpB,YAAY,EAAE,MAAM,GAAG,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,EAC7C,OAAO,CAAC,EAAE;IACR,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,SAAS,GAAG,MAAM,CAAA;IAC3B,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;CACzC,KACE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,aAAa,EAAE,eAAe,CAAC,eAAe,CAyBtF,CAAA;;;;;;;;;;AAED,qBAAa,QAAS,SAAQ,aAM5B;CAAG"}
|
package/dist/node/cmd.js
CHANGED
|
@@ -1,24 +1,54 @@
|
|
|
1
1
|
import { isNotUndefined, shouldNeverHappen } from '@livestore/utils';
|
|
2
2
|
import { Command, Effect, identity, Schema } from '@livestore/utils/effect';
|
|
3
|
+
import { applyLoggingToCommand } from "./cmd-log.js";
|
|
3
4
|
export const cmd = Effect.fn('cmd')(function* (commandInput, options) {
|
|
4
5
|
const cwd = options?.cwd ?? process.env.WORKSPACE_ROOT ?? shouldNeverHappen('WORKSPACE_ROOT is not set');
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
const asArray = Array.isArray(commandInput);
|
|
7
|
+
const parts = asArray ? commandInput.filter(isNotUndefined) : undefined;
|
|
8
|
+
const [command, ...args] = asArray ? parts : commandInput.split(' ');
|
|
8
9
|
const debugEnvStr = Object.entries(options?.env ?? {})
|
|
9
10
|
.map(([key, value]) => `${key}='${value}' `)
|
|
10
11
|
.join('');
|
|
11
|
-
|
|
12
|
-
const
|
|
12
|
+
// Compose command with optional tee logging via helper
|
|
13
|
+
const loggingOpts = {
|
|
14
|
+
...(options?.logDir ? { logDir: options.logDir } : {}),
|
|
15
|
+
...(options?.logFileName ? { logFileName: options.logFileName } : {}),
|
|
16
|
+
...(options?.logRetention ? { logRetention: options.logRetention } : {}),
|
|
17
|
+
};
|
|
18
|
+
const { input: finalInput, subshell: needsShell } = yield* applyLoggingToCommand(commandInput, loggingOpts);
|
|
19
|
+
const subshell = (options?.shell ? true : false) || needsShell;
|
|
20
|
+
const commandDebugStr = debugEnvStr + (Array.isArray(finalInput) ? finalInput.join(' ') : finalInput);
|
|
21
|
+
const subshellStr = subshell ? ' (in subshell)' : '';
|
|
13
22
|
yield* Effect.logDebug(`Running '${commandDebugStr}' in '${cwd}'${subshellStr}`);
|
|
14
|
-
yield* Effect.annotateCurrentSpan({
|
|
15
|
-
|
|
23
|
+
yield* Effect.annotateCurrentSpan({
|
|
24
|
+
'span.label': commandDebugStr,
|
|
25
|
+
cwd,
|
|
26
|
+
command,
|
|
27
|
+
args,
|
|
28
|
+
logDir: options?.logDir,
|
|
29
|
+
});
|
|
30
|
+
const makeAndRun = (input, useShell) => {
|
|
31
|
+
if (Array.isArray(input)) {
|
|
32
|
+
const [c, ...a] = input;
|
|
33
|
+
return Command.make(c, ...a);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
if (useShell) {
|
|
37
|
+
// Pipeline / tee requires shell
|
|
38
|
+
return Command.make(input);
|
|
39
|
+
}
|
|
40
|
+
// No shell: split into executable and args
|
|
41
|
+
const [c, ...a] = input.split(' ');
|
|
42
|
+
return Command.make(c, ...a);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return yield* makeAndRun(finalInput, subshell).pipe(
|
|
16
46
|
// TODO don't forward abort signal to the command
|
|
17
47
|
Command.stdin('inherit'), // Forward stdin to the command
|
|
18
48
|
// inherit = Stream stdout to process.stdout, pipe = Stream stdout to process.stderr
|
|
19
49
|
Command.stdout(options?.stdout ?? 'inherit'),
|
|
20
50
|
// inherit = Stream stderr to process.stderr, pipe = Stream stderr to process.stdout
|
|
21
|
-
Command.stderr(options?.stderr ?? 'inherit'), Command.workingDirectory(cwd),
|
|
51
|
+
Command.stderr(options?.stderr ?? 'inherit'), Command.workingDirectory(cwd), subshell ? Command.runInShell(true) : identity, Command.env(options?.env ?? {}), Command.exitCode, Effect.tap((exitCode) => exitCode === 0
|
|
22
52
|
? Effect.void
|
|
23
53
|
: Effect.fail(CmdError.make({
|
|
24
54
|
command: command,
|
package/dist/node/cmd.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cmd.js","sourceRoot":"","sources":["../../src/node/cmd.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpE,OAAO,EAAE,OAAO,EAAwB,MAAM,EAAE,QAAQ,EAAsB,MAAM,EAAE,MAAM,yBAAyB,CAAA;AAErH,MAAM,CAAC,MAAM,GAAG,
|
|
1
|
+
{"version":3,"file":"cmd.js","sourceRoot":"","sources":["../../src/node/cmd.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cmd.test.d.ts","sourceRoot":"","sources":["../../src/node/cmd.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { Effect } from '@livestore/utils/effect';
|
|
4
|
+
import { PlatformNode } from '@livestore/utils/node';
|
|
5
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest';
|
|
6
|
+
import { expect } from 'vitest';
|
|
7
|
+
import { cmd } from "./cmd.js";
|
|
8
|
+
const withNode = Vitest.makeWithTestCtx({
|
|
9
|
+
makeLayer: () => PlatformNode.NodeContext.layer,
|
|
10
|
+
timeout: 20_000,
|
|
11
|
+
});
|
|
12
|
+
Vitest.describe('cmd helper', () => {
|
|
13
|
+
Vitest.scopedLive('runs tokenized string without shell', (test) => Effect.gen(function* () {
|
|
14
|
+
const exit = yield* cmd('printf ok');
|
|
15
|
+
expect(Number(exit)).toBe(0);
|
|
16
|
+
}).pipe(withNode(test)));
|
|
17
|
+
Vitest.scopedLive('runs array input', (test) => Effect.gen(function* () {
|
|
18
|
+
const exit = yield* cmd(['printf', 'ok']);
|
|
19
|
+
expect(Number(exit)).toBe(0);
|
|
20
|
+
}).pipe(withNode(test)));
|
|
21
|
+
Vitest.scopedLive('supports logging with archive + retention', (test) => Effect.gen(function* () {
|
|
22
|
+
const workspace = process.env.WORKSPACE_ROOT;
|
|
23
|
+
const logsDir = path.join(workspace, 'tmp', 'cmd-tests', String(Date.now()));
|
|
24
|
+
// first run
|
|
25
|
+
const exit1 = yield* cmd('printf first', { logDir: logsDir });
|
|
26
|
+
expect(Number(exit1)).toBe(0);
|
|
27
|
+
const current = path.join(logsDir, 'dev.log');
|
|
28
|
+
expect(fs.existsSync(current)).toBe(true);
|
|
29
|
+
expect(fs.readFileSync(current, 'utf8')).toBe('first');
|
|
30
|
+
// second run — archives previous
|
|
31
|
+
const exit2 = yield* cmd('printf second', { logDir: logsDir });
|
|
32
|
+
expect(Number(exit2)).toBe(0);
|
|
33
|
+
const archiveDir = path.join(logsDir, 'archive');
|
|
34
|
+
const archives = fs.readdirSync(archiveDir).filter((f) => f.endsWith('.log'));
|
|
35
|
+
expect(archives.length).toBe(1);
|
|
36
|
+
const archivedPath = path.join(archiveDir, archives[0]);
|
|
37
|
+
expect(fs.readFileSync(archivedPath, 'utf8')).toBe('first');
|
|
38
|
+
expect(fs.readFileSync(current, 'utf8')).toBe('second');
|
|
39
|
+
// generate many archives to exercise retention (keep 50)
|
|
40
|
+
for (let i = 0; i < 60; i++) {
|
|
41
|
+
// Use small unique payloads
|
|
42
|
+
yield* cmd(['printf', String(i)], { logDir: logsDir });
|
|
43
|
+
}
|
|
44
|
+
const archivesAfter = fs.readdirSync(archiveDir).filter((f) => f.endsWith('.log'));
|
|
45
|
+
expect(archivesAfter.length).toBeLessThanOrEqual(50);
|
|
46
|
+
}).pipe(withNode(test)));
|
|
47
|
+
});
|
|
48
|
+
//# sourceMappingURL=cmd.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cmd.test.js","sourceRoot":"","sources":["../../src/node/cmd.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livestore/utils-dev",
|
|
3
|
-
"version": "0.0.0-snapshot-
|
|
3
|
+
"version": "0.0.0-snapshot-ae29f95eb90e37e6b0ae25179f2fc8e6f6c238cd",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"./src/node-vitest/global.ts",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"@opentelemetry/sdk-trace-base": "2.0.1",
|
|
23
23
|
"@opentelemetry/sdk-trace-node": "2.0.1",
|
|
24
24
|
"wrangler": "4.38.0",
|
|
25
|
-
"@livestore/utils": "0.0.0-snapshot-
|
|
25
|
+
"@livestore/utils": "0.0.0-snapshot-ae29f95eb90e37e6b0ae25179f2fc8e6f6c238cd"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {},
|
|
28
28
|
"files": [
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { isNotUndefined } from '@livestore/utils'
|
|
5
|
+
import { Effect, identity } from '@livestore/utils/effect'
|
|
6
|
+
|
|
7
|
+
export type TCmdLoggingOptions = {
|
|
8
|
+
readonly logDir?: string
|
|
9
|
+
readonly logFileName?: string
|
|
10
|
+
readonly logRetention?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const shellEscape = (s: string): string => (/[A-Za-z0-9_./:=+@-]+/.test(s) ? s : `'${s.replaceAll("'", `'"'"'`)}'`)
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Prepares logging directories, archives previous canonical log and prunes archives.
|
|
17
|
+
* Returns the canonical current log path if logging is enabled, otherwise undefined.
|
|
18
|
+
*/
|
|
19
|
+
export const prepareCmdLogging: (options: TCmdLoggingOptions) => Effect.Effect<string | undefined, never, never> =
|
|
20
|
+
Effect.fn('cmd.logging.prepare')(function* ({
|
|
21
|
+
logDir,
|
|
22
|
+
logFileName = 'dev.log',
|
|
23
|
+
logRetention = 50,
|
|
24
|
+
}: TCmdLoggingOptions) {
|
|
25
|
+
if (!logDir || logDir === '') return undefined as string | undefined
|
|
26
|
+
|
|
27
|
+
const logsDir = logDir
|
|
28
|
+
const archiveDir = path.join(logsDir, 'archive')
|
|
29
|
+
const currentLogPath = path.join(logsDir, logFileName)
|
|
30
|
+
|
|
31
|
+
// Ensure directories exist
|
|
32
|
+
yield* Effect.sync(() => fs.mkdirSync(archiveDir, { recursive: true }))
|
|
33
|
+
|
|
34
|
+
// Archive previous log if present
|
|
35
|
+
if (fs.existsSync(currentLogPath)) {
|
|
36
|
+
const safeIso = new Date().toISOString().replaceAll(':', '-')
|
|
37
|
+
const archivedBase = `${path.parse(logFileName).name}-${safeIso}.log`
|
|
38
|
+
const archivedLog = path.join(archiveDir, archivedBase)
|
|
39
|
+
yield* Effect.try({ try: () => fs.renameSync(currentLogPath, archivedLog), catch: identity }).pipe(
|
|
40
|
+
Effect.catchAll(() =>
|
|
41
|
+
Effect.try({
|
|
42
|
+
try: () => {
|
|
43
|
+
fs.copyFileSync(currentLogPath, archivedLog)
|
|
44
|
+
fs.truncateSync(currentLogPath, 0)
|
|
45
|
+
},
|
|
46
|
+
catch: identity,
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
49
|
+
Effect.ignore,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// Prune archives to retain only the newest N
|
|
53
|
+
yield* Effect.try({ try: () => fs.readdirSync(archiveDir), catch: identity }).pipe(
|
|
54
|
+
Effect.map((names) => names.filter((n) => n.endsWith('.log'))),
|
|
55
|
+
Effect.map((names) =>
|
|
56
|
+
names
|
|
57
|
+
.map((name) => ({ name, mtimeMs: fs.statSync(path.join(archiveDir, name)).mtimeMs }))
|
|
58
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs),
|
|
59
|
+
),
|
|
60
|
+
Effect.flatMap((entries) =>
|
|
61
|
+
Effect.forEach(entries.slice(logRetention), (e) =>
|
|
62
|
+
Effect.try({ try: () => fs.unlinkSync(path.join(archiveDir, e.name)), catch: identity }).pipe(
|
|
63
|
+
Effect.ignore,
|
|
64
|
+
),
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
Effect.ignore,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return currentLogPath
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Given a command input, applies logging by piping output through `tee` to the
|
|
76
|
+
* canonical log file. Returns the transformed input and whether a shell is required.
|
|
77
|
+
*/
|
|
78
|
+
export const applyLoggingToCommand: (
|
|
79
|
+
commandInput: string | (string | undefined)[],
|
|
80
|
+
options: TCmdLoggingOptions,
|
|
81
|
+
) => Effect.Effect<{ input: string | string[]; subshell: boolean; logPath?: string }, never, never> = Effect.fn(
|
|
82
|
+
'cmd.logging.apply',
|
|
83
|
+
)(function* (commandInput, options) {
|
|
84
|
+
const asArray = Array.isArray(commandInput)
|
|
85
|
+
const parts = asArray ? (commandInput as (string | undefined)[]).filter(isNotUndefined) : undefined
|
|
86
|
+
|
|
87
|
+
const logPath = yield* prepareCmdLogging(options)
|
|
88
|
+
if (!logPath) {
|
|
89
|
+
return {
|
|
90
|
+
input: asArray ? ((parts as string[]) ?? []) : (commandInput as string),
|
|
91
|
+
subshell: false,
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const input = asArray
|
|
96
|
+
? [...((parts as string[]) ?? []), '2>&1', '|', 'tee', logPath]
|
|
97
|
+
: `${commandInput as string} 2>&1 | tee ${shellEscape(logPath)}`
|
|
98
|
+
|
|
99
|
+
return { input, subshell: true, logPath }
|
|
100
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { Effect } from '@livestore/utils/effect'
|
|
5
|
+
import { PlatformNode } from '@livestore/utils/node'
|
|
6
|
+
import { Vitest } from '@livestore/utils-dev/node-vitest'
|
|
7
|
+
import { expect } from 'vitest'
|
|
8
|
+
import { cmd } from './cmd.ts'
|
|
9
|
+
|
|
10
|
+
const withNode = Vitest.makeWithTestCtx({
|
|
11
|
+
makeLayer: () => PlatformNode.NodeContext.layer,
|
|
12
|
+
timeout: 20_000,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
Vitest.describe('cmd helper', () => {
|
|
16
|
+
Vitest.scopedLive('runs tokenized string without shell', (test) =>
|
|
17
|
+
Effect.gen(function* () {
|
|
18
|
+
const exit = yield* cmd('printf ok')
|
|
19
|
+
expect(Number(exit)).toBe(0)
|
|
20
|
+
}).pipe(withNode(test)),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
Vitest.scopedLive('runs array input', (test) =>
|
|
24
|
+
Effect.gen(function* () {
|
|
25
|
+
const exit = yield* cmd(['printf', 'ok'])
|
|
26
|
+
expect(Number(exit)).toBe(0)
|
|
27
|
+
}).pipe(withNode(test)),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
Vitest.scopedLive('supports logging with archive + retention', (test) =>
|
|
31
|
+
Effect.gen(function* () {
|
|
32
|
+
const workspace = process.env.WORKSPACE_ROOT!
|
|
33
|
+
const logsDir = path.join(workspace, 'tmp', 'cmd-tests', String(Date.now()))
|
|
34
|
+
|
|
35
|
+
// first run
|
|
36
|
+
const exit1 = yield* cmd('printf first', { logDir: logsDir })
|
|
37
|
+
expect(Number(exit1)).toBe(0)
|
|
38
|
+
const current = path.join(logsDir, 'dev.log')
|
|
39
|
+
expect(fs.existsSync(current)).toBe(true)
|
|
40
|
+
expect(fs.readFileSync(current, 'utf8')).toBe('first')
|
|
41
|
+
|
|
42
|
+
// second run — archives previous
|
|
43
|
+
const exit2 = yield* cmd('printf second', { logDir: logsDir })
|
|
44
|
+
expect(Number(exit2)).toBe(0)
|
|
45
|
+
const archiveDir = path.join(logsDir, 'archive')
|
|
46
|
+
const archives = fs.readdirSync(archiveDir).filter((f) => f.endsWith('.log'))
|
|
47
|
+
expect(archives.length).toBe(1)
|
|
48
|
+
const archivedPath = path.join(archiveDir, archives[0]!)
|
|
49
|
+
expect(fs.readFileSync(archivedPath, 'utf8')).toBe('first')
|
|
50
|
+
expect(fs.readFileSync(current, 'utf8')).toBe('second')
|
|
51
|
+
|
|
52
|
+
// generate many archives to exercise retention (keep 50)
|
|
53
|
+
for (let i = 0; i < 60; i++) {
|
|
54
|
+
// Use small unique payloads
|
|
55
|
+
yield* cmd(['printf', String(i)], { logDir: logsDir })
|
|
56
|
+
}
|
|
57
|
+
const archivesAfter = fs.readdirSync(archiveDir).filter((f) => f.endsWith('.log'))
|
|
58
|
+
expect(archivesAfter.length).toBeLessThanOrEqual(50)
|
|
59
|
+
}).pipe(withNode(test)),
|
|
60
|
+
)
|
|
61
|
+
})
|
package/src/node/cmd.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { isNotUndefined, shouldNeverHappen } from '@livestore/utils'
|
|
2
2
|
import { Command, type CommandExecutor, Effect, identity, type PlatformError, Schema } from '@livestore/utils/effect'
|
|
3
3
|
|
|
4
|
+
import { applyLoggingToCommand } from './cmd-log.ts'
|
|
5
|
+
|
|
4
6
|
export const cmd: (
|
|
5
7
|
commandInput: string | (string | undefined)[],
|
|
6
8
|
options?:
|
|
@@ -10,25 +12,68 @@ export const cmd: (
|
|
|
10
12
|
stdout?: 'inherit' | 'pipe'
|
|
11
13
|
shell?: boolean
|
|
12
14
|
env?: Record<string, string | undefined>
|
|
15
|
+
/**
|
|
16
|
+
* When provided, streams command output to terminal AND to a canonical log file (`${logDir}/dev.log`) in this directory.
|
|
17
|
+
* Also archives the previous run to `${logDir}/archive/dev-<ISO>.log` and keeps only the latest 50 archives.
|
|
18
|
+
*/
|
|
19
|
+
logDir?: string
|
|
20
|
+
/** Optional basename for the canonical log file; defaults to 'dev.log' */
|
|
21
|
+
logFileName?: string
|
|
22
|
+
/** Optional number of archived logs to retain; defaults to 50 */
|
|
23
|
+
logRetention?: number
|
|
13
24
|
}
|
|
14
25
|
| undefined,
|
|
15
26
|
) => Effect.Effect<CommandExecutor.ExitCode, PlatformError.PlatformError | CmdError, CommandExecutor.CommandExecutor> =
|
|
16
27
|
Effect.fn('cmd')(function* (commandInput, options) {
|
|
17
28
|
const cwd = options?.cwd ?? process.env.WORKSPACE_ROOT ?? shouldNeverHappen('WORKSPACE_ROOT is not set')
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
|
|
30
|
+
const asArray = Array.isArray(commandInput)
|
|
31
|
+
const parts = asArray ? (commandInput as (string | undefined)[]).filter(isNotUndefined) : undefined
|
|
32
|
+
const [command, ...args] = asArray ? (parts as string[]) : (commandInput as string).split(' ')
|
|
21
33
|
|
|
22
34
|
const debugEnvStr = Object.entries(options?.env ?? {})
|
|
23
35
|
.map(([key, value]) => `${key}='${value}' `)
|
|
24
36
|
.join('')
|
|
25
|
-
|
|
26
|
-
|
|
37
|
+
|
|
38
|
+
// Compose command with optional tee logging via helper
|
|
39
|
+
const loggingOpts = {
|
|
40
|
+
...(options?.logDir ? { logDir: options.logDir } : {}),
|
|
41
|
+
...(options?.logFileName ? { logFileName: options.logFileName } : {}),
|
|
42
|
+
...(options?.logRetention ? { logRetention: options.logRetention } : {}),
|
|
43
|
+
} as const
|
|
44
|
+
const { input: finalInput, subshell: needsShell } = yield* applyLoggingToCommand(commandInput, loggingOpts)
|
|
45
|
+
|
|
46
|
+
const subshell = (options?.shell ? true : false) || needsShell
|
|
47
|
+
|
|
48
|
+
const commandDebugStr =
|
|
49
|
+
debugEnvStr + (Array.isArray(finalInput) ? (finalInput as string[]).join(' ') : (finalInput as string))
|
|
50
|
+
const subshellStr = subshell ? ' (in subshell)' : ''
|
|
27
51
|
|
|
28
52
|
yield* Effect.logDebug(`Running '${commandDebugStr}' in '${cwd}'${subshellStr}`)
|
|
29
|
-
yield* Effect.annotateCurrentSpan({
|
|
53
|
+
yield* Effect.annotateCurrentSpan({
|
|
54
|
+
'span.label': commandDebugStr,
|
|
55
|
+
cwd,
|
|
56
|
+
command,
|
|
57
|
+
args,
|
|
58
|
+
logDir: options?.logDir,
|
|
59
|
+
})
|
|
30
60
|
|
|
31
|
-
|
|
61
|
+
const makeAndRun = (input: string | string[], useShell: boolean) => {
|
|
62
|
+
if (Array.isArray(input)) {
|
|
63
|
+
const [c, ...a] = input
|
|
64
|
+
return Command.make(c!, ...a)
|
|
65
|
+
} else {
|
|
66
|
+
if (useShell) {
|
|
67
|
+
// Pipeline / tee requires shell
|
|
68
|
+
return Command.make(input)
|
|
69
|
+
}
|
|
70
|
+
// No shell: split into executable and args
|
|
71
|
+
const [c, ...a] = input.split(' ')
|
|
72
|
+
return Command.make(c!, ...a)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return yield* makeAndRun(finalInput, subshell).pipe(
|
|
32
77
|
// TODO don't forward abort signal to the command
|
|
33
78
|
Command.stdin('inherit'), // Forward stdin to the command
|
|
34
79
|
// inherit = Stream stdout to process.stdout, pipe = Stream stdout to process.stderr
|
|
@@ -36,7 +81,7 @@ export const cmd: (
|
|
|
36
81
|
// inherit = Stream stderr to process.stderr, pipe = Stream stderr to process.stdout
|
|
37
82
|
Command.stderr(options?.stderr ?? 'inherit'),
|
|
38
83
|
Command.workingDirectory(cwd),
|
|
39
|
-
|
|
84
|
+
subshell ? Command.runInShell(true) : identity,
|
|
40
85
|
Command.env(options?.env ?? {}),
|
|
41
86
|
Command.exitCode,
|
|
42
87
|
Effect.tap((exitCode) =>
|