poe-code 3.0.307 → 3.0.309

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-code",
3
- "version": "3.0.307",
3
+ "version": "3.0.309",
4
4
  "description": "CLI tool to configure Poe API for developer workflows.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -2,8 +2,10 @@ import net from "node:net";
2
2
  export async function waitForReady(check, options) {
3
3
  assertValidTimeout(options.timeoutMs, "readiness timeout");
4
4
  if (check.kind === "log-pattern") {
5
+ assertValidLogPattern(check.pattern);
5
6
  return waitForLogPattern(check.pattern, options);
6
7
  }
8
+ assertValidTcpPort(check.port);
7
9
  return waitForTcp(check, options);
8
10
  }
9
11
  function waitForLogPattern(pattern, options) {
@@ -124,3 +126,13 @@ function assertValidTimeout(value, description) {
124
126
  throw new Error(`Invalid ${description}: ${value}`);
125
127
  }
126
128
  }
129
+ function assertValidLogPattern(value) {
130
+ if (value.trim().length === 0) {
131
+ throw new Error("Invalid log pattern readiness check: pattern must not be blank.");
132
+ }
133
+ }
134
+ function assertValidTcpPort(value) {
135
+ if (!Number.isSafeInteger(value) || value <= 0 || value > 65_535) {
136
+ throw new Error(`Invalid TCP readiness port: ${value}`);
137
+ }
138
+ }
@@ -6,7 +6,7 @@ import { hasOwnErrorCode } from "./errors.js";
6
6
  import { createLogWriter } from "./logs/log-writer.js";
7
7
  import { assertPathHasNoSymbolicLinks } from "./path-safety.js";
8
8
  import { assertValidManagedProcessId } from "./process-id.js";
9
- import { createStateStore } from "./state/state-store.js";
9
+ import { assertValidProcessStateDocument, createStateStore } from "./state/state-store.js";
10
10
  import { createSupervisor } from "./supervisor/supervisor.js";
11
11
  const DEFAULT_POLL_INTERVAL_MS = 100;
12
12
  const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
@@ -14,6 +14,7 @@ const DEFAULT_STOP_TIMEOUT_MS = 5_000;
14
14
  const TEMP_WRITE_MAX_ATTEMPTS = 3;
15
15
  export async function startManagedProcess(options) {
16
16
  assertOptionalFiniteDuration(options.startupTimeoutMs, "startup timeout");
17
+ assertOptionalFiniteDuration(options.pollIntervalMs, "poll interval");
17
18
  const fs = options.fs ?? defaultFs();
18
19
  const spec = normalizeSpec(options.spec);
19
20
  const existing = await readManagedProcess({
@@ -56,6 +57,8 @@ export async function startManagedProcess(options) {
56
57
  }
57
58
  }
58
59
  export async function stopManagedProcess(options) {
60
+ assertOptionalFiniteDuration(options.stopTimeoutMs, "stop timeout");
61
+ assertOptionalFiniteDuration(options.pollIntervalMs, "poll interval");
59
62
  const fs = options.fs ?? defaultFs();
60
63
  const record = await readManagedProcess({
61
64
  baseDir: options.baseDir,
@@ -177,7 +180,18 @@ export async function* followManagedLogs(options) {
177
180
  throw new Error(`Invalid managed log poll interval: ${pollIntervalMs}`);
178
181
  }
179
182
  const cursor = createFollowLogCursor();
180
- await primeFollowCursor(fs, options.baseDir, options.id, stream, cursor);
183
+ const initialLines = options.lines === undefined
184
+ ? []
185
+ : await readInitialFollowLogWindow(fs, options.baseDir, options.id, stream, options.lines, cursor);
186
+ if (options.lines === undefined) {
187
+ await primeFollowCursor(fs, options.baseDir, options.id, stream, cursor);
188
+ }
189
+ for (const line of initialLines) {
190
+ if (options.signal?.aborted) {
191
+ return;
192
+ }
193
+ yield line;
194
+ }
181
195
  while (!options.signal?.aborted) {
182
196
  await sleep(pollIntervalMs);
183
197
  if (options.signal?.aborted) {
@@ -197,6 +211,26 @@ function createFollowLogCursor() {
197
211
  remainder: ""
198
212
  };
199
213
  }
214
+ async function readInitialFollowLogWindow(fs, baseDir, id, stream, lines, cursor) {
215
+ assertValidLogLineCount(lines);
216
+ const stat = await statFollowedLog(fs, baseDir, id, stream);
217
+ resetFollowCursor(cursor, stat?.fileId ?? null);
218
+ if (stat === null) {
219
+ return [];
220
+ }
221
+ const bytes = await readFollowedLogBytes(fs, resolveCurrentLogPath(baseDir, id, stream), 0);
222
+ cursor.offset = bytes.byteLength;
223
+ const allLines = consumeFollowedLogBytes(cursor, bytes);
224
+ if (lines === 0) {
225
+ return [];
226
+ }
227
+ return allLines.slice(-lines);
228
+ }
229
+ function assertValidLogLineCount(lines) {
230
+ if (!Number.isFinite(lines) || !Number.isInteger(lines) || lines < 0) {
231
+ throw new Error("lines must be a finite non-negative integer");
232
+ }
233
+ }
200
234
  async function primeFollowCursor(fs, baseDir, id, stream, cursor) {
201
235
  const stat = await statFollowedLog(fs, baseDir, id, stream);
202
236
  resetFollowCursor(cursor, stat?.fileId ?? null);
@@ -310,6 +344,7 @@ export async function runManagedProcess(options) {
310
344
  if (options.signal?.aborted) {
311
345
  return;
312
346
  }
347
+ assertOptionalFiniteDuration(options.pollIntervalMs, "poll interval");
313
348
  const fs = options.fs ?? defaultFs();
314
349
  await assertProcessDirectorySafe(fs, options.baseDir, options.id);
315
350
  await assertPathNotSymbolicLink(fs, resolveLogDir(options.baseDir, options.id));
@@ -523,13 +558,94 @@ async function readSpec(fs, baseDir, id) {
523
558
  if (!isRecord(spec) || typeof spec.id !== "string" || spec.id !== id) {
524
559
  throw new Error(`Invalid managed process specification for "${id}".`);
525
560
  }
526
- return spec;
561
+ return assertValidProcessSpec(spec, id);
562
+ }
563
+ function assertValidProcessSpec(value, id) {
564
+ if (value.id !== id ||
565
+ !isNonEmptyString(value.command) ||
566
+ !isOptionalStringArray(value.args) ||
567
+ !isOptionalString(value.cwd) ||
568
+ !isOptionalStringRecord(value.env) ||
569
+ !isRestartPolicy(value.restart) ||
570
+ !isOptionalNonNegativeSafeInteger(value.maxRestarts) ||
571
+ !isOptionalFiniteDurationValue(value.backoffMs) ||
572
+ !isOptionalFiniteDurationValue(value.maxBackoffMs) ||
573
+ !isOptionalPositiveSafeInteger(value.logRetainCount) ||
574
+ !isOptionalReadyCheck(value.readyCheck) ||
575
+ !isOptionalRecord(value.docker)) {
576
+ throw new Error(`Invalid managed process specification for "${id}".`);
577
+ }
578
+ return value;
579
+ }
580
+ function isNonEmptyString(value) {
581
+ return typeof value === "string" && value.trim().length > 0;
582
+ }
583
+ function isOptionalString(value) {
584
+ return value === undefined || typeof value === "string";
585
+ }
586
+ function isOptionalStringArray(value) {
587
+ return value === undefined || (Array.isArray(value) &&
588
+ value.every((entry) => typeof entry === "string"));
589
+ }
590
+ function isOptionalStringRecord(value) {
591
+ if (value === undefined) {
592
+ return true;
593
+ }
594
+ if (!isPlainRecord(value)) {
595
+ return false;
596
+ }
597
+ return Object.values(value).every((entry) => typeof entry === "string");
598
+ }
599
+ function isRestartPolicy(value) {
600
+ return value === "never" || value === "on-failure" || value === "always";
601
+ }
602
+ function isOptionalNonNegativeSafeInteger(value) {
603
+ return value === undefined || (typeof value === "number" &&
604
+ Number.isSafeInteger(value) &&
605
+ value >= 0);
606
+ }
607
+ function isOptionalPositiveSafeInteger(value) {
608
+ return value === undefined || (typeof value === "number" &&
609
+ Number.isSafeInteger(value) &&
610
+ value > 0);
611
+ }
612
+ function isOptionalFiniteDurationValue(value) {
613
+ return value === undefined || (typeof value === "number" &&
614
+ Number.isFinite(value) &&
615
+ value >= 0);
616
+ }
617
+ function isOptionalReadyCheck(value) {
618
+ if (value === undefined) {
619
+ return true;
620
+ }
621
+ if (!isPlainRecord(value)) {
622
+ return false;
623
+ }
624
+ if (value.kind === "log-pattern") {
625
+ return isNonEmptyString(value.pattern);
626
+ }
627
+ if (value.kind === "tcp") {
628
+ return (typeof value.port === "number" &&
629
+ Number.isSafeInteger(value.port) &&
630
+ value.port > 0 &&
631
+ value.port <= 65_535 &&
632
+ isOptionalString(value.host) &&
633
+ isOptionalFiniteDurationValue(value.timeoutMs));
634
+ }
635
+ return false;
636
+ }
637
+ function isOptionalRecord(value) {
638
+ return value === undefined || isPlainRecord(value);
639
+ }
640
+ function isPlainRecord(value) {
641
+ return typeof value === "object" && value !== null && !Array.isArray(value);
527
642
  }
528
643
  async function writeSpec(fs, baseDir, spec) {
529
644
  await writeJsonFile(fs, resolveSpecPath(baseDir, spec.id), spec);
530
645
  }
531
646
  async function readState(fs, baseDir, id) {
532
- return await readJsonFile(fs, resolveStatePath(baseDir, id));
647
+ const state = await readJsonFile(fs, resolveStatePath(baseDir, id));
648
+ return state === null ? null : assertValidProcessStateDocument(state, id);
533
649
  }
534
650
  async function writeState(fs, baseDir, state) {
535
651
  await writeJsonFile(fs, resolveStatePath(baseDir, state.id), state);
@@ -1,6 +1,20 @@
1
1
  import path from "node:path";
2
2
  export function assertValidManagedProcessId(id) {
3
- if (id.length === 0 || id === "." || id === ".." || path.basename(id) !== id) {
3
+ if (id.length === 0 ||
4
+ id !== id.trim() ||
5
+ id === "." ||
6
+ id === ".." ||
7
+ path.basename(id) !== id ||
8
+ hasControlCharacter(id)) {
4
9
  throw new Error(`Invalid managed process id: ${id}`);
5
10
  }
6
11
  }
12
+ function hasControlCharacter(value) {
13
+ for (const char of value) {
14
+ const code = char.charCodeAt(0);
15
+ if (code <= 31 || code === 127) {
16
+ return true;
17
+ }
18
+ }
19
+ return false;
20
+ }
@@ -1,2 +1,3 @@
1
- import type { LauncherFileSystem, StateStore } from "../types.js";
1
+ import type { LauncherFileSystem, ProcessState, StateStore } from "../types.js";
2
2
  export declare function createStateStore(stateDir: string, fs?: LauncherFileSystem): StateStore;
3
+ export declare function assertValidProcessStateDocument(value: unknown, id: string): ProcessState;
@@ -78,10 +78,7 @@ export function createStateStore(stateDir, fs = nodeFs) {
78
78
  await assertPathHasNoSymbolicLinks(fs, statePath);
79
79
  const content = await fs.readFile(statePath, "utf8");
80
80
  const parsed = JSON.parse(content);
81
- if (!isProcessState(parsed, id)) {
82
- throw new Error(`Invalid process state document: ${id}`);
83
- }
84
- return parsed;
81
+ return assertValidProcessStateDocument(parsed, id);
85
82
  }
86
83
  catch (error) {
87
84
  if (isNotFoundError(error)) {
@@ -169,6 +166,12 @@ export function createStateStore(stateDir, fs = nodeFs) {
169
166
  }
170
167
  return { read, write, list, remove };
171
168
  }
169
+ export function assertValidProcessStateDocument(value, id) {
170
+ if (!isProcessState(value, id)) {
171
+ throw new Error(`Invalid process state document: ${id}`);
172
+ }
173
+ return value;
174
+ }
172
175
  function isProcessState(value, id) {
173
176
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
174
177
  return false;
@@ -181,11 +184,20 @@ function isProcessState(value, id) {
181
184
  state.status === "crashed" ||
182
185
  state.status === "restarting") &&
183
186
  (state.runtime === "host" || state.runtime === "docker") &&
184
- typeof state.restartCount === "number" &&
185
- (state.lastExitCode === null || typeof state.lastExitCode === "number") &&
187
+ isPositiveSafeIntegerOrNull(state.pid) &&
188
+ isNonNegativeSafeInteger(state.restartCount) &&
189
+ (state.lastExitCode === null || isNonNegativeSafeInteger(state.lastExitCode)) &&
186
190
  (state.lastStartedAt === null || typeof state.lastStartedAt === "string") &&
187
191
  (state.lastStoppedAt === null || typeof state.lastStoppedAt === "string") &&
188
192
  typeof state.command === "string" &&
189
193
  Array.isArray(state.args) &&
190
194
  state.args.every((argument) => typeof argument === "string"));
191
195
  }
196
+ function isPositiveSafeIntegerOrNull(value) {
197
+ return value === null || (typeof value === "number" &&
198
+ Number.isSafeInteger(value) &&
199
+ value > 0);
200
+ }
201
+ function isNonNegativeSafeInteger(value) {
202
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
203
+ }