@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.
@@ -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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"}
@@ -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;
@@ -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;AAErH,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;CACzC,GACD,SAAS,KACV,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,QAAQ,EAAE,aAAa,CAAC,aAAa,GAAG,QAAQ,EAAE,eAAe,CAAC,eAAe,CAyChH,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"}
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 [command, ...args] = Array.isArray(commandInput)
6
- ? commandInput.filter(isNotUndefined)
7
- : commandInput.split(' ');
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
- const subshellStr = options?.shell ? ' (in subshell)' : '';
12
- const commandDebugStr = debugEnvStr + [command, ...args].join(' ');
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({ 'span.label': commandDebugStr, cwd, command, args });
15
- return yield* Command.make(command, ...args).pipe(
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), options?.shell ? Command.runInShell(true) : identity, Command.env(options?.env ?? {}), Command.exitCode, Effect.tap((exitCode) => exitCode === 0
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,
@@ -1 +1 @@
1
- {"version":3,"file":"cmd.js","sourceRoot":"","sources":["../../src/node/cmd.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cmd.js","sourceRoot":"","sources":["../../src/node/cmd.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=cmd.test.d.ts.map
@@ -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-c0f03c6e0f72a2bf15dde9a0b7d25352f79da9aa",
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-c0f03c6e0f72a2bf15dde9a0b7d25352f79da9aa"
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
- const [command, ...args] = Array.isArray(commandInput)
19
- ? commandInput.filter(isNotUndefined)
20
- : commandInput.split(' ')
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
- const subshellStr = options?.shell ? ' (in subshell)' : ''
26
- const commandDebugStr = debugEnvStr + [command, ...args].join(' ')
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({ 'span.label': commandDebugStr, cwd, command, args })
53
+ yield* Effect.annotateCurrentSpan({
54
+ 'span.label': commandDebugStr,
55
+ cwd,
56
+ command,
57
+ args,
58
+ logDir: options?.logDir,
59
+ })
30
60
 
31
- return yield* Command.make(command!, ...args).pipe(
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
- options?.shell ? Command.runInShell(true) : identity,
84
+ subshell ? Command.runInShell(true) : identity,
40
85
  Command.env(options?.env ?? {}),
41
86
  Command.exitCode,
42
87
  Effect.tap((exitCode) =>