poe-code 3.0.306 → 3.0.308
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 +106 -14
- package/dist/index.js.map +3 -3
- package/dist/metafile.json +1 -1
- package/package.json +1 -1
- package/packages/process-launcher/dist/health/health-check.js +12 -0
- package/packages/process-launcher/dist/launcher.js +88 -3
- package/packages/process-launcher/dist/process-id.js +15 -1
- package/packages/process-launcher/dist/state/state-store.d.ts +2 -1
- package/packages/process-launcher/dist/state/state-store.js +18 -6
- package/packages/toolcraft-landing-page/dist/render.js +99 -1
- package/packages/toolcraft-landing-page/dist/template.js +6 -4
package/package.json
CHANGED
|
@@ -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,
|
|
@@ -310,6 +313,7 @@ export async function runManagedProcess(options) {
|
|
|
310
313
|
if (options.signal?.aborted) {
|
|
311
314
|
return;
|
|
312
315
|
}
|
|
316
|
+
assertOptionalFiniteDuration(options.pollIntervalMs, "poll interval");
|
|
313
317
|
const fs = options.fs ?? defaultFs();
|
|
314
318
|
await assertProcessDirectorySafe(fs, options.baseDir, options.id);
|
|
315
319
|
await assertPathNotSymbolicLink(fs, resolveLogDir(options.baseDir, options.id));
|
|
@@ -523,13 +527,94 @@ async function readSpec(fs, baseDir, id) {
|
|
|
523
527
|
if (!isRecord(spec) || typeof spec.id !== "string" || spec.id !== id) {
|
|
524
528
|
throw new Error(`Invalid managed process specification for "${id}".`);
|
|
525
529
|
}
|
|
526
|
-
return spec;
|
|
530
|
+
return assertValidProcessSpec(spec, id);
|
|
531
|
+
}
|
|
532
|
+
function assertValidProcessSpec(value, id) {
|
|
533
|
+
if (value.id !== id ||
|
|
534
|
+
!isNonEmptyString(value.command) ||
|
|
535
|
+
!isOptionalStringArray(value.args) ||
|
|
536
|
+
!isOptionalString(value.cwd) ||
|
|
537
|
+
!isOptionalStringRecord(value.env) ||
|
|
538
|
+
!isRestartPolicy(value.restart) ||
|
|
539
|
+
!isOptionalNonNegativeSafeInteger(value.maxRestarts) ||
|
|
540
|
+
!isOptionalFiniteDurationValue(value.backoffMs) ||
|
|
541
|
+
!isOptionalFiniteDurationValue(value.maxBackoffMs) ||
|
|
542
|
+
!isOptionalPositiveSafeInteger(value.logRetainCount) ||
|
|
543
|
+
!isOptionalReadyCheck(value.readyCheck) ||
|
|
544
|
+
!isOptionalRecord(value.docker)) {
|
|
545
|
+
throw new Error(`Invalid managed process specification for "${id}".`);
|
|
546
|
+
}
|
|
547
|
+
return value;
|
|
548
|
+
}
|
|
549
|
+
function isNonEmptyString(value) {
|
|
550
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
551
|
+
}
|
|
552
|
+
function isOptionalString(value) {
|
|
553
|
+
return value === undefined || typeof value === "string";
|
|
554
|
+
}
|
|
555
|
+
function isOptionalStringArray(value) {
|
|
556
|
+
return value === undefined || (Array.isArray(value) &&
|
|
557
|
+
value.every((entry) => typeof entry === "string"));
|
|
558
|
+
}
|
|
559
|
+
function isOptionalStringRecord(value) {
|
|
560
|
+
if (value === undefined) {
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
if (!isPlainRecord(value)) {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
return Object.values(value).every((entry) => typeof entry === "string");
|
|
567
|
+
}
|
|
568
|
+
function isRestartPolicy(value) {
|
|
569
|
+
return value === "never" || value === "on-failure" || value === "always";
|
|
570
|
+
}
|
|
571
|
+
function isOptionalNonNegativeSafeInteger(value) {
|
|
572
|
+
return value === undefined || (typeof value === "number" &&
|
|
573
|
+
Number.isSafeInteger(value) &&
|
|
574
|
+
value >= 0);
|
|
575
|
+
}
|
|
576
|
+
function isOptionalPositiveSafeInteger(value) {
|
|
577
|
+
return value === undefined || (typeof value === "number" &&
|
|
578
|
+
Number.isSafeInteger(value) &&
|
|
579
|
+
value > 0);
|
|
580
|
+
}
|
|
581
|
+
function isOptionalFiniteDurationValue(value) {
|
|
582
|
+
return value === undefined || (typeof value === "number" &&
|
|
583
|
+
Number.isFinite(value) &&
|
|
584
|
+
value >= 0);
|
|
585
|
+
}
|
|
586
|
+
function isOptionalReadyCheck(value) {
|
|
587
|
+
if (value === undefined) {
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
if (!isPlainRecord(value)) {
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
if (value.kind === "log-pattern") {
|
|
594
|
+
return isNonEmptyString(value.pattern);
|
|
595
|
+
}
|
|
596
|
+
if (value.kind === "tcp") {
|
|
597
|
+
return (typeof value.port === "number" &&
|
|
598
|
+
Number.isSafeInteger(value.port) &&
|
|
599
|
+
value.port > 0 &&
|
|
600
|
+
value.port <= 65_535 &&
|
|
601
|
+
isOptionalString(value.host) &&
|
|
602
|
+
isOptionalFiniteDurationValue(value.timeoutMs));
|
|
603
|
+
}
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
function isOptionalRecord(value) {
|
|
607
|
+
return value === undefined || isPlainRecord(value);
|
|
608
|
+
}
|
|
609
|
+
function isPlainRecord(value) {
|
|
610
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
527
611
|
}
|
|
528
612
|
async function writeSpec(fs, baseDir, spec) {
|
|
529
613
|
await writeJsonFile(fs, resolveSpecPath(baseDir, spec.id), spec);
|
|
530
614
|
}
|
|
531
615
|
async function readState(fs, baseDir, id) {
|
|
532
|
-
|
|
616
|
+
const state = await readJsonFile(fs, resolveStatePath(baseDir, id));
|
|
617
|
+
return state === null ? null : assertValidProcessStateDocument(state, id);
|
|
533
618
|
}
|
|
534
619
|
async function writeState(fs, baseDir, state) {
|
|
535
620
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
(state.
|
|
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
|
+
}
|
|
@@ -3,10 +3,108 @@ import { highlight } from "./highlight.js";
|
|
|
3
3
|
import { SCRIPT } from "./script.js";
|
|
4
4
|
import { CSS } from "./styles.js";
|
|
5
5
|
import { TEMPLATE } from "./template.js";
|
|
6
|
+
const DEFAULT_ACCENT = "#2563eb";
|
|
7
|
+
function isAsciiHexDigit(value) {
|
|
8
|
+
const code = value.charCodeAt(0);
|
|
9
|
+
return ((code >= 48 && code <= 57) ||
|
|
10
|
+
(code >= 65 && code <= 70) ||
|
|
11
|
+
(code >= 97 && code <= 102));
|
|
12
|
+
}
|
|
13
|
+
function isHexColor(value) {
|
|
14
|
+
if (!value.startsWith("#")) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (![4, 5, 7, 9].includes(value.length)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
for (const char of value.slice(1)) {
|
|
21
|
+
if (!isAsciiHexDigit(char)) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
function isNamedColor(value) {
|
|
28
|
+
if (value.length === 0) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
for (const char of value) {
|
|
32
|
+
const code = char.charCodeAt(0);
|
|
33
|
+
if (!((code >= 65 && code <= 90) || (code >= 97 && code <= 122))) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
function sanitizeAccent(value) {
|
|
40
|
+
const trimmed = value.trim();
|
|
41
|
+
if (isHexColor(trimmed) || isNamedColor(trimmed)) {
|
|
42
|
+
return trimmed;
|
|
43
|
+
}
|
|
44
|
+
return DEFAULT_ACCENT;
|
|
45
|
+
}
|
|
46
|
+
function hasUnsafeHrefCharacter(value) {
|
|
47
|
+
for (const char of value) {
|
|
48
|
+
const code = char.charCodeAt(0);
|
|
49
|
+
if (code <= 32 || code === 127) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
function schemeDelimiterIndex(value) {
|
|
56
|
+
const colon = value.indexOf(":");
|
|
57
|
+
if (colon === -1) {
|
|
58
|
+
return -1;
|
|
59
|
+
}
|
|
60
|
+
const slash = value.indexOf("/");
|
|
61
|
+
const question = value.indexOf("?");
|
|
62
|
+
const hash = value.indexOf("#");
|
|
63
|
+
const precedingDelimiters = [slash, question, hash].filter((index) => index !== -1);
|
|
64
|
+
if (precedingDelimiters.some((index) => index < colon)) {
|
|
65
|
+
return -1;
|
|
66
|
+
}
|
|
67
|
+
return colon;
|
|
68
|
+
}
|
|
69
|
+
function safeHref(value) {
|
|
70
|
+
if (value === undefined) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const trimmed = value.trim();
|
|
74
|
+
if (trimmed.length === 0 || hasUnsafeHrefCharacter(trimmed)) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
const delimiter = schemeDelimiterIndex(trimmed);
|
|
78
|
+
if (delimiter === -1) {
|
|
79
|
+
return trimmed;
|
|
80
|
+
}
|
|
81
|
+
const scheme = trimmed.slice(0, delimiter).toLowerCase();
|
|
82
|
+
return scheme === "http" || scheme === "https" ? trimmed : undefined;
|
|
83
|
+
}
|
|
84
|
+
function stripFragment(value) {
|
|
85
|
+
const hash = value.indexOf("#");
|
|
86
|
+
return hash === -1 ? value : value.slice(0, hash);
|
|
87
|
+
}
|
|
88
|
+
function docsHref(value) {
|
|
89
|
+
return stripFragment(safeHref(value) ?? "#docs");
|
|
90
|
+
}
|
|
91
|
+
function docsAnchorHref(baseHref, anchor) {
|
|
92
|
+
return baseHref === "#docs" ? baseHref : `${baseHref}#${anchor}`;
|
|
93
|
+
}
|
|
6
94
|
export function renderLandingPage(page) {
|
|
7
|
-
const
|
|
95
|
+
const accent = sanitizeAccent(page.accent);
|
|
96
|
+
const docsUrl = docsHref(page.docsUrl);
|
|
97
|
+
const styles = renderTemplate(CSS, { accent });
|
|
8
98
|
const view = {
|
|
9
99
|
...page,
|
|
100
|
+
accent,
|
|
101
|
+
repoUrl: safeHref(page.repoUrl),
|
|
102
|
+
docsUrl,
|
|
103
|
+
docsHelloWorldUrl: docsAnchorHref(docsUrl, "hello-world"),
|
|
104
|
+
docsRuntimeUrl: docsAnchorHref(docsUrl, "one-binary-three-runtimes"),
|
|
105
|
+
docsSecretsUrl: docsAnchorHref(docsUrl, "secrets"),
|
|
106
|
+
docsMigrationUrl: docsAnchorHref(docsUrl, "migrating-from-a-folder-of-scripts"),
|
|
107
|
+
copyInstall: page.includeJs && page.install !== undefined,
|
|
10
108
|
installHtml: page.install === undefined ? undefined : highlight(page.install),
|
|
11
109
|
useCases: page.useCases.map((useCase) => ({
|
|
12
110
|
...useCase,
|
|
@@ -33,7 +33,9 @@ export const TEMPLATE = String.raw `<!doctype html>
|
|
|
33
33
|
{{#install}}
|
|
34
34
|
<div class="install">
|
|
35
35
|
<code><span aria-hidden="true">$ </span>{{{installHtml}}}</code>
|
|
36
|
+
{{#copyInstall}}
|
|
36
37
|
<button class="copy" type="button" data-copy="{{install}}" aria-label="Copy {{install}}">Copy</button>
|
|
38
|
+
{{/copyInstall}}
|
|
37
39
|
</div>
|
|
38
40
|
{{/install}}
|
|
39
41
|
</div>
|
|
@@ -112,10 +114,10 @@ export const TEMPLATE = String.raw `<!doctype html>
|
|
|
112
114
|
<a class="text-link" href="{{docsUrl}}">Read the Toolcraft README <span aria-hidden="true">→</span></a>
|
|
113
115
|
</div>
|
|
114
116
|
<div class="docs-grid">
|
|
115
|
-
<a class="doc-card" href="{{
|
|
116
|
-
<a class="doc-card" href="{{
|
|
117
|
-
<a class="doc-card" href="{{
|
|
118
|
-
<a class="doc-card" href="{{
|
|
117
|
+
<a class="doc-card" href="{{docsHelloWorldUrl}}"><span>01</span><strong>Start with one command</strong><small>Install, define, and run a typed CLI in five minutes.</small></a>
|
|
118
|
+
<a class="doc-card" href="{{docsRuntimeUrl}}"><span>02</span><strong>Choose a runtime</strong><small>CLI, MCP, and SDK from the same command tree.</small></a>
|
|
119
|
+
<a class="doc-card" href="{{docsSecretsUrl}}"><span>03</span><strong>Add safety controls</strong><small>Secrets, preconditions, services, and human approval.</small></a>
|
|
120
|
+
<a class="doc-card" href="{{docsMigrationUrl}}"><span>04</span><strong>Migrate existing scripts</strong><small>Adopt Toolcraft incrementally without rewriting useful logic.</small></a>
|
|
119
121
|
</div>
|
|
120
122
|
</div>
|
|
121
123
|
</section>
|