@xfey/tutti 0.1.42 → 0.1.44
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/artifacts/candidate-validator.d.ts +39 -0
- package/dist/artifacts/candidate-validator.js +512 -0
- package/dist/artifacts/index.d.ts +3 -1
- package/dist/artifacts/index.js +3 -1
- package/dist/artifacts/manifest-contract.d.ts +30 -0
- package/dist/artifacts/manifest-contract.js +214 -0
- package/dist/artifacts/manifest.d.ts +3 -12
- package/dist/artifacts/manifest.js +29 -201
- package/dist/artifacts/preview-runtime.d.ts +29 -10
- package/dist/artifacts/preview-runtime.js +400 -155
- package/dist/artifacts/process-boundary.d.ts +33 -0
- package/dist/artifacts/process-boundary.js +134 -0
- package/dist/control-plane/types.d.ts +7 -2
- package/dist/prompt-templates/index.d.ts +1 -1
- package/dist/prompt-templates/index.js +1 -0
- package/dist/provider-usage/index.js +1 -0
- package/dist/run-pipeline/artifact-applicability.d.ts +49 -0
- package/dist/run-pipeline/artifact-applicability.js +281 -0
- package/dist/run-pipeline/openai.d.ts +6 -0
- package/dist/run-pipeline/openai.js +72 -2
- package/dist/run-pipeline/promotion-reconcile.d.ts +9 -1
- package/dist/run-pipeline/promotion-reconcile.js +65 -11
- package/dist/server-shell/cli/launch.d.ts +2 -0
- package/dist/server-shell/cli/launch.js +7 -1
- package/dist/server-shell/http/host-tunnel.js +1 -1
- package/dist/server-shell/http/routes/project-api/artifacts-routes.js +63 -14
- package/dist/server-shell/http/routes/project-api/types.d.ts +2 -0
- package/dist/server-shell/local-console/project-service.d.ts +5 -0
- package/dist/server-shell/local-console/project-service.js +25 -0
- package/dist/server-shell/local-console/server.js +10 -0
- package/dist/workspace-ops/index.d.ts +1 -1
- package/dist/workspace-ops/index.js +1 -1
- package/dist/workspace-ops/run-workspaces.d.ts +1 -0
- package/dist/workspace-ops/run-workspaces.js +27 -0
- package/node_modules/@tutti/shared/dist/schemas/api/artifacts.d.ts +28 -8
- package/node_modules/@tutti/shared/dist/schemas/api/artifacts.js +34 -9
- package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +5 -1
- package/node_modules/@tutti/shared/dist/utils/redaction/index.d.ts +1 -1
- package/node_modules/@tutti/shared/dist/utils/redaction/index.js +14 -0
- package/package.json +1 -1
- package/prompts/README.md +1 -0
- package/prompts/artifacts/README.md +7 -0
- package/prompts/artifacts/applicability.md +41 -0
- package/prompts/prompt-flow-map.md +29 -26
- package/prompts/runs/README.md +3 -3
- package/prompts/runs/task-continuation.md +36 -4
- package/prompts/runs/task-retry.md +42 -6
- package/prompts/runs/task-run.md +36 -4
- package/web/assets/index-B7I1QwT_.js +69 -0
- package/web/assets/index-F-ouhuJ5.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-Sqw_t67u.css +0 -1
- package/web/assets/index-cYUjFgtc.js +0 -29
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { Value } from "@sinclair/typebox/value";
|
|
3
|
+
import { ArtifactManifestSchema } from "@tutti/shared/schemas/api";
|
|
4
|
+
import { classifyViewerPath } from "@tutti/shared/utils";
|
|
5
|
+
export const ARTIFACT_MANIFEST_PATH = "tutti.artifact.json";
|
|
6
|
+
export const MAX_ARTIFACT_MANIFEST_BYTES = 64 * 1024;
|
|
7
|
+
const HTML_ENTRY_PATTERN = /\.html?$/iu;
|
|
8
|
+
const STATIC_ARTIFACT_FIELDS = new Set(["title", "kind", "root", "entry", "network"]);
|
|
9
|
+
const SERVER_ARTIFACT_FIELDS = new Set(["title", "kind", "cwd", "entry", "network"]);
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function unexpectedArtifactField(artifact, allowedFields) {
|
|
14
|
+
return Object.keys(artifact).find((field) => !allowedFields.has(field)) ?? null;
|
|
15
|
+
}
|
|
16
|
+
function artifactSchemaHint(value) {
|
|
17
|
+
if (!isRecord(value)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const artifact = value.artifact;
|
|
21
|
+
if (!isRecord(artifact)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
const title = artifact.title;
|
|
25
|
+
if (typeof title !== "string" || title.trim().length === 0) {
|
|
26
|
+
return "artifact.title is required";
|
|
27
|
+
}
|
|
28
|
+
if (title.length > 120) {
|
|
29
|
+
return "artifact.title must be 120 characters or less";
|
|
30
|
+
}
|
|
31
|
+
if (artifact.kind === "static") {
|
|
32
|
+
if (typeof artifact.root !== "string" || artifact.root.length === 0) {
|
|
33
|
+
return "static artifact root is required";
|
|
34
|
+
}
|
|
35
|
+
if (typeof artifact.entry !== "string" || artifact.entry.length === 0) {
|
|
36
|
+
return "static artifact entry is required";
|
|
37
|
+
}
|
|
38
|
+
if (artifact.network === undefined) {
|
|
39
|
+
return "static artifact network is required";
|
|
40
|
+
}
|
|
41
|
+
if (typeof artifact.network !== "boolean") {
|
|
42
|
+
return "static artifact network must be a boolean";
|
|
43
|
+
}
|
|
44
|
+
const unexpected = unexpectedArtifactField(artifact, STATIC_ARTIFACT_FIELDS);
|
|
45
|
+
return unexpected === null ? null : `static artifact field "${unexpected}" is not allowed`;
|
|
46
|
+
}
|
|
47
|
+
if (artifact.kind === "server") {
|
|
48
|
+
if (typeof artifact.cwd !== "string" || artifact.cwd.length === 0) {
|
|
49
|
+
return "server artifact cwd is required";
|
|
50
|
+
}
|
|
51
|
+
if (typeof artifact.entry !== "string" || artifact.entry.length === 0) {
|
|
52
|
+
return "server artifact entry is required";
|
|
53
|
+
}
|
|
54
|
+
if (!artifact.entry.startsWith("/")) {
|
|
55
|
+
return "server artifact entry must start with /";
|
|
56
|
+
}
|
|
57
|
+
if (artifact.network === undefined) {
|
|
58
|
+
return "server artifact network is required";
|
|
59
|
+
}
|
|
60
|
+
if (typeof artifact.network !== "boolean") {
|
|
61
|
+
return "server artifact network must be a boolean";
|
|
62
|
+
}
|
|
63
|
+
const unexpected = unexpectedArtifactField(artifact, SERVER_ARTIFACT_FIELDS);
|
|
64
|
+
return unexpected === null ? null : `server artifact field "${unexpected}" is not allowed`;
|
|
65
|
+
}
|
|
66
|
+
return 'artifact.kind must be "static" or "server"';
|
|
67
|
+
}
|
|
68
|
+
function firstSchemaError(value) {
|
|
69
|
+
const hint = artifactSchemaHint(value);
|
|
70
|
+
if (hint !== null) {
|
|
71
|
+
return hint;
|
|
72
|
+
}
|
|
73
|
+
const first = [...Value.Errors(ArtifactManifestSchema, value)][0];
|
|
74
|
+
if (first === undefined) {
|
|
75
|
+
return "Artifact manifest does not match schema";
|
|
76
|
+
}
|
|
77
|
+
const path = first.path.length > 0 ? `${first.path}: ` : "";
|
|
78
|
+
return `${path}${first.message}`;
|
|
79
|
+
}
|
|
80
|
+
function decodeUtf8(buffer) {
|
|
81
|
+
try {
|
|
82
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function normalizeRepoPath(options) {
|
|
89
|
+
const classification = classifyViewerPath(options.path);
|
|
90
|
+
if (classification.kind === "rejected") {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: { code: "invalid_path", message: `${options.label} is invalid` },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (!options.allowRoot && classification.normalized_path === "") {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
error: { code: "invalid_path", message: `${options.label} cannot be repo root` },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (classification.normalized_path.split("/").some((segment) => segment.startsWith("."))) {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
error: { code: "invalid_path", message: `${options.label} cannot use hidden paths` },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
return { ok: true, path: classification.normalized_path };
|
|
109
|
+
}
|
|
110
|
+
function validateServerEntry(entry) {
|
|
111
|
+
if (entry.includes("\\") || entry.includes("#") || entry.startsWith("//")) {
|
|
112
|
+
return { code: "invalid_entry", message: "Server artifact entry is not a safe HTTP path" };
|
|
113
|
+
}
|
|
114
|
+
let parsed;
|
|
115
|
+
try {
|
|
116
|
+
parsed = new URL(entry, "http://artifact.invalid");
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return { code: "invalid_entry", message: "Server artifact entry is not a safe HTTP path" };
|
|
120
|
+
}
|
|
121
|
+
if (parsed.origin !== "http://artifact.invalid" || !entry.startsWith("/")) {
|
|
122
|
+
return { code: "invalid_entry", message: "Server artifact entry is not a safe HTTP path" };
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
export function parseArtifactManifestContract(buffer) {
|
|
127
|
+
if (buffer.byteLength > MAX_ARTIFACT_MANIFEST_BYTES) {
|
|
128
|
+
return { ok: false, error: { code: "too_large", message: "Artifact manifest is too large" } };
|
|
129
|
+
}
|
|
130
|
+
const text = decodeUtf8(buffer);
|
|
131
|
+
if (text === null) {
|
|
132
|
+
return {
|
|
133
|
+
ok: false,
|
|
134
|
+
error: { code: "unsupported_encoding", message: "Artifact manifest must be UTF-8 JSON" },
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
let parsed;
|
|
138
|
+
try {
|
|
139
|
+
parsed = JSON.parse(text);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const detail = error instanceof Error ? error.message : "Artifact manifest is not valid JSON";
|
|
143
|
+
return { ok: false, error: { code: "invalid_json", message: detail } };
|
|
144
|
+
}
|
|
145
|
+
if (!Value.Check(ArtifactManifestSchema, parsed)) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
error: { code: "invalid_schema", message: firstSchemaError(parsed) },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
const manifest = parsed;
|
|
152
|
+
const artifact = manifest.artifact;
|
|
153
|
+
if (artifact.kind === "static") {
|
|
154
|
+
const root = normalizeRepoPath({
|
|
155
|
+
path: artifact.root,
|
|
156
|
+
label: "Static artifact root",
|
|
157
|
+
allowRoot: true,
|
|
158
|
+
});
|
|
159
|
+
if (!root.ok) {
|
|
160
|
+
return root;
|
|
161
|
+
}
|
|
162
|
+
const entry = normalizeRepoPath({
|
|
163
|
+
path: artifact.entry,
|
|
164
|
+
label: "Static artifact entry",
|
|
165
|
+
allowRoot: false,
|
|
166
|
+
});
|
|
167
|
+
if (!entry.ok) {
|
|
168
|
+
return entry;
|
|
169
|
+
}
|
|
170
|
+
const entryRepoPath = root.path === "" ? entry.path : posix.join(root.path, entry.path);
|
|
171
|
+
if (!HTML_ENTRY_PATTERN.test(entryRepoPath)) {
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
error: { code: "invalid_entry", message: "Static artifact entry must be an HTML file" },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
declaration: {
|
|
180
|
+
manifest,
|
|
181
|
+
runtime: {
|
|
182
|
+
kind: "static",
|
|
183
|
+
root_path: root.path,
|
|
184
|
+
entry_path: entry.path,
|
|
185
|
+
entry_repo_path: entryRepoPath,
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const cwd = normalizeRepoPath({
|
|
191
|
+
path: artifact.cwd,
|
|
192
|
+
label: "Server artifact cwd",
|
|
193
|
+
allowRoot: true,
|
|
194
|
+
});
|
|
195
|
+
if (!cwd.ok) {
|
|
196
|
+
return cwd;
|
|
197
|
+
}
|
|
198
|
+
const entryError = validateServerEntry(artifact.entry);
|
|
199
|
+
if (entryError !== null) {
|
|
200
|
+
return { ok: false, error: entryError };
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
declaration: {
|
|
205
|
+
manifest,
|
|
206
|
+
runtime: {
|
|
207
|
+
kind: "server",
|
|
208
|
+
cwd_path: cwd.path,
|
|
209
|
+
entry_path: artifact.entry,
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=manifest-contract.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ArtifactManifest, ArtifactProjection, GetArtifactsResponse } from "@tutti/shared/schemas/api";
|
|
2
|
-
|
|
2
|
+
import { type ValidatedArtifactRuntime } from "./manifest-contract.js";
|
|
3
|
+
export { ARTIFACT_MANIFEST_PATH } from "./manifest-contract.js";
|
|
3
4
|
export declare const ARTIFACT_PREVIEW_BASE_PATH: "/artifacts/preview";
|
|
4
5
|
type ArtifactInvalidProjection = Extract<ArtifactProjection, {
|
|
5
6
|
status: "invalid";
|
|
@@ -7,16 +8,7 @@ type ArtifactInvalidProjection = Extract<ArtifactProjection, {
|
|
|
7
8
|
type ArtifactDeclaredProjection = Extract<ArtifactProjection, {
|
|
8
9
|
status: "declared";
|
|
9
10
|
}>;
|
|
10
|
-
export type DeclaredArtifactRuntime =
|
|
11
|
-
kind: "static";
|
|
12
|
-
root_path: string;
|
|
13
|
-
entry_path: string;
|
|
14
|
-
entry_repo_path: string;
|
|
15
|
-
} | {
|
|
16
|
-
kind: "server";
|
|
17
|
-
cwd_path: string;
|
|
18
|
-
ready_path: string;
|
|
19
|
-
};
|
|
11
|
+
export type DeclaredArtifactRuntime = ValidatedArtifactRuntime;
|
|
20
12
|
export type ArtifactDeclaration = {
|
|
21
13
|
status: "not_declared";
|
|
22
14
|
projection: Extract<ArtifactProjection, {
|
|
@@ -44,5 +36,4 @@ export declare function buildArtifactPreviewUrl(options: {
|
|
|
44
36
|
}): string;
|
|
45
37
|
export declare function readArtifactDeclaration(options: ReadArtifactDeclarationOptions): ArtifactDeclaration;
|
|
46
38
|
export declare function readArtifactProjection(options: ReadArtifactDeclarationOptions): GetArtifactsResponse;
|
|
47
|
-
export {};
|
|
48
39
|
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import { posix } from "node:path";
|
|
2
|
-
import { Value } from "@sinclair/typebox/value";
|
|
3
|
-
import { ArtifactManifestSchema } from "@tutti/shared/schemas/api";
|
|
4
|
-
import { WorkspaceOpsError } from "../workspace-ops/errors.js";
|
|
5
1
|
import { entryKind, lsPath, readBlob, readViewerSnapshot, requireFileEntry, } from "../workspace-ops/viewer-git.js";
|
|
6
|
-
import {
|
|
7
|
-
export
|
|
2
|
+
import { ARTIFACT_MANIFEST_PATH, parseArtifactManifestContract, } from "./manifest-contract.js";
|
|
3
|
+
export { ARTIFACT_MANIFEST_PATH } from "./manifest-contract.js";
|
|
8
4
|
export const ARTIFACT_PREVIEW_BASE_PATH = "/artifacts/preview";
|
|
9
|
-
const MAX_ARTIFACT_MANIFEST_BYTES = 64 * 1024;
|
|
10
|
-
const HTML_ENTRY_PATTERN = /\.html?$/iu;
|
|
11
5
|
function invalidArtifact(code, message) {
|
|
12
6
|
return {
|
|
13
7
|
status: "invalid",
|
|
@@ -28,105 +22,6 @@ function truncateMessage(message) {
|
|
|
28
22
|
}
|
|
29
23
|
return `${normalized.slice(0, 217)}...`;
|
|
30
24
|
}
|
|
31
|
-
function decodeUtf8(buffer) {
|
|
32
|
-
try {
|
|
33
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
function isRecord(value) {
|
|
40
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
41
|
-
}
|
|
42
|
-
function artifactSchemaHint(value) {
|
|
43
|
-
if (!isRecord(value)) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
const artifact = value.artifact;
|
|
47
|
-
if (!isRecord(artifact)) {
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
const title = artifact.title;
|
|
51
|
-
if (typeof title !== "string" || title.trim().length === 0) {
|
|
52
|
-
return "artifact.title is required";
|
|
53
|
-
}
|
|
54
|
-
if (title.length > 120) {
|
|
55
|
-
return "artifact.title must be 120 characters or less";
|
|
56
|
-
}
|
|
57
|
-
if (artifact.network !== undefined && typeof artifact.network !== "boolean") {
|
|
58
|
-
return "artifact.network must be a boolean";
|
|
59
|
-
}
|
|
60
|
-
const kind = artifact.kind;
|
|
61
|
-
if (kind === "static") {
|
|
62
|
-
if (typeof artifact.root !== "string" || artifact.root.length === 0) {
|
|
63
|
-
return "static artifact root is required";
|
|
64
|
-
}
|
|
65
|
-
if (typeof artifact.entry !== "string" || artifact.entry.length === 0) {
|
|
66
|
-
return "static artifact entry is required";
|
|
67
|
-
}
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
if (kind === "server") {
|
|
71
|
-
if (typeof artifact.cwd !== "string" || artifact.cwd.length === 0) {
|
|
72
|
-
return "server artifact cwd is required";
|
|
73
|
-
}
|
|
74
|
-
if (artifact.script !== "artifact:preview") {
|
|
75
|
-
return 'server artifact script must be "artifact:preview"';
|
|
76
|
-
}
|
|
77
|
-
if (artifact.ready_path !== undefined &&
|
|
78
|
-
(typeof artifact.ready_path !== "string" ||
|
|
79
|
-
artifact.ready_path.length === 0 ||
|
|
80
|
-
artifact.ready_path.length > 256 ||
|
|
81
|
-
!artifact.ready_path.startsWith("/"))) {
|
|
82
|
-
return "server artifact ready_path must start with /";
|
|
83
|
-
}
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
return 'artifact.kind must be "static" or "server"';
|
|
87
|
-
}
|
|
88
|
-
function firstSchemaError(value) {
|
|
89
|
-
const hint = artifactSchemaHint(value);
|
|
90
|
-
if (hint !== null) {
|
|
91
|
-
return hint;
|
|
92
|
-
}
|
|
93
|
-
const first = [...Value.Errors(ArtifactManifestSchema, value)][0];
|
|
94
|
-
if (first === undefined) {
|
|
95
|
-
return "Artifact manifest does not match schema";
|
|
96
|
-
}
|
|
97
|
-
const path = first.path.length > 0 ? `${first.path}: ` : "";
|
|
98
|
-
return `${path}${first.message}`;
|
|
99
|
-
}
|
|
100
|
-
function normalizeArtifactPath(options) {
|
|
101
|
-
try {
|
|
102
|
-
const normalized = requireNormalizedViewerPath(options.path);
|
|
103
|
-
if (!options.allowRoot && normalized === "") {
|
|
104
|
-
return {
|
|
105
|
-
ok: false,
|
|
106
|
-
declaration: invalidArtifact("invalid_path", `${options.label} cannot be repo root`),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
if (hasHiddenSegment(normalized)) {
|
|
110
|
-
return {
|
|
111
|
-
ok: false,
|
|
112
|
-
declaration: invalidArtifact("invalid_path", `${options.label} cannot use hidden paths`),
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
return { ok: true, path: normalized };
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
if (error instanceof WorkspaceOpsError) {
|
|
119
|
-
return {
|
|
120
|
-
ok: false,
|
|
121
|
-
declaration: invalidArtifact("invalid_path", `${options.label} is invalid`),
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
throw error;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
function hasHiddenSegment(path) {
|
|
128
|
-
return path.split("/").some((segment) => segment.startsWith("."));
|
|
129
|
-
}
|
|
130
25
|
function ensureDirectory(options) {
|
|
131
26
|
if (options.path === "") {
|
|
132
27
|
return null;
|
|
@@ -141,27 +36,14 @@ function ensureDirectory(options) {
|
|
|
141
36
|
return null;
|
|
142
37
|
}
|
|
143
38
|
function ensureStaticEntry(options) {
|
|
144
|
-
const
|
|
145
|
-
if (entryRepoPath === "" || !HTML_ENTRY_PATTERN.test(entryRepoPath)) {
|
|
146
|
-
return {
|
|
147
|
-
ok: false,
|
|
148
|
-
declaration: invalidArtifact("invalid_entry", "Static artifact entry must be an HTML file"),
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
const entry = lsPath(options.workspaceRoot, entryRepoPath);
|
|
39
|
+
const entry = lsPath(options.workspaceRoot, options.entryRepoPath);
|
|
152
40
|
if (entry === null) {
|
|
153
|
-
return
|
|
154
|
-
ok: false,
|
|
155
|
-
declaration: invalidArtifact("not_found", "Static artifact entry does not exist"),
|
|
156
|
-
};
|
|
41
|
+
return invalidArtifact("not_found", "Static artifact entry does not exist");
|
|
157
42
|
}
|
|
158
43
|
if (entryKind(entry) !== "file") {
|
|
159
|
-
return
|
|
160
|
-
ok: false,
|
|
161
|
-
declaration: invalidArtifact("invalid_entry", "Static artifact entry is not a file"),
|
|
162
|
-
};
|
|
44
|
+
return invalidArtifact("invalid_entry", "Static artifact entry is not a file");
|
|
163
45
|
}
|
|
164
|
-
return
|
|
46
|
+
return null;
|
|
165
47
|
}
|
|
166
48
|
function encodePreviewPath(path) {
|
|
167
49
|
return path
|
|
@@ -184,9 +66,7 @@ function createDeclaredProjection(options) {
|
|
|
184
66
|
url: buildArtifactPreviewUrl({
|
|
185
67
|
entryPath: options.runtime.entry_path,
|
|
186
68
|
manifestBlobOid: options.manifestBlobOid,
|
|
187
|
-
...(options.previewBasePath === undefined
|
|
188
|
-
? {}
|
|
189
|
-
: { basePath: options.previewBasePath }),
|
|
69
|
+
...(options.previewBasePath === undefined ? {} : { basePath: options.previewBasePath }),
|
|
190
70
|
}),
|
|
191
71
|
}
|
|
192
72
|
: { status: "stopped" };
|
|
@@ -197,94 +77,40 @@ function createDeclaredProjection(options) {
|
|
|
197
77
|
artifact: {
|
|
198
78
|
title: artifact.title,
|
|
199
79
|
kind: artifact.kind,
|
|
200
|
-
network: artifact.network
|
|
80
|
+
network: artifact.network,
|
|
201
81
|
},
|
|
202
82
|
preview,
|
|
203
83
|
};
|
|
204
84
|
}
|
|
205
|
-
function
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
const text = decodeUtf8(buffer);
|
|
210
|
-
if (text === null) {
|
|
211
|
-
return invalidArtifact("unsupported_encoding", "Artifact manifest must be UTF-8 JSON");
|
|
212
|
-
}
|
|
213
|
-
let parsed;
|
|
214
|
-
try {
|
|
215
|
-
parsed = JSON.parse(text);
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
return invalidArtifact("invalid_json", "Artifact manifest is not valid JSON");
|
|
219
|
-
}
|
|
220
|
-
if (!Value.Check(ArtifactManifestSchema, parsed)) {
|
|
221
|
-
return invalidArtifact("invalid_schema", firstSchemaError(parsed));
|
|
222
|
-
}
|
|
223
|
-
return parsed;
|
|
224
|
-
}
|
|
225
|
-
function validateArtifactManifest(options) {
|
|
226
|
-
const artifact = options.manifest.artifact;
|
|
227
|
-
if (artifact.kind === "static") {
|
|
228
|
-
const root = normalizeArtifactPath({
|
|
229
|
-
path: artifact.root,
|
|
230
|
-
label: "Static artifact root",
|
|
231
|
-
allowRoot: true,
|
|
232
|
-
});
|
|
233
|
-
if (!root.ok) {
|
|
234
|
-
return root.declaration;
|
|
235
|
-
}
|
|
236
|
-
const entry = normalizeArtifactPath({
|
|
237
|
-
path: artifact.entry,
|
|
238
|
-
label: "Static artifact entry",
|
|
239
|
-
allowRoot: false,
|
|
240
|
-
});
|
|
241
|
-
if (!entry.ok) {
|
|
242
|
-
return entry.declaration;
|
|
243
|
-
}
|
|
85
|
+
function validateCommittedArtifact(options) {
|
|
86
|
+
const runtime = options.runtime;
|
|
87
|
+
if (runtime.kind === "static") {
|
|
244
88
|
const rootError = ensureDirectory({
|
|
245
89
|
workspaceRoot: options.workspaceRoot,
|
|
246
|
-
path:
|
|
90
|
+
path: runtime.root_path,
|
|
247
91
|
label: "Static artifact root",
|
|
248
92
|
});
|
|
249
93
|
if (rootError !== null) {
|
|
250
94
|
return rootError;
|
|
251
95
|
}
|
|
252
|
-
const
|
|
96
|
+
const entryError = ensureStaticEntry({
|
|
253
97
|
workspaceRoot: options.workspaceRoot,
|
|
254
|
-
|
|
255
|
-
entryPath: entry.path,
|
|
98
|
+
entryRepoPath: runtime.entry_repo_path,
|
|
256
99
|
});
|
|
257
|
-
if (
|
|
258
|
-
return
|
|
100
|
+
if (entryError !== null) {
|
|
101
|
+
return entryError;
|
|
259
102
|
}
|
|
260
|
-
return
|
|
261
|
-
kind: "static",
|
|
262
|
-
root_path: root.path,
|
|
263
|
-
entry_path: entry.path,
|
|
264
|
-
entry_repo_path: entryResult.entryRepoPath,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
const cwd = normalizeArtifactPath({
|
|
268
|
-
path: artifact.cwd,
|
|
269
|
-
label: "Server artifact cwd",
|
|
270
|
-
allowRoot: true,
|
|
271
|
-
});
|
|
272
|
-
if (!cwd.ok) {
|
|
273
|
-
return cwd.declaration;
|
|
103
|
+
return runtime;
|
|
274
104
|
}
|
|
275
105
|
const cwdError = ensureDirectory({
|
|
276
106
|
workspaceRoot: options.workspaceRoot,
|
|
277
|
-
path:
|
|
107
|
+
path: runtime.cwd_path,
|
|
278
108
|
label: "Server artifact cwd",
|
|
279
109
|
});
|
|
280
110
|
if (cwdError !== null) {
|
|
281
111
|
return cwdError;
|
|
282
112
|
}
|
|
283
|
-
return
|
|
284
|
-
kind: "server",
|
|
285
|
-
cwd_path: cwd.path,
|
|
286
|
-
ready_path: artifact.ready_path ?? "/",
|
|
287
|
-
};
|
|
113
|
+
return runtime;
|
|
288
114
|
}
|
|
289
115
|
export function readArtifactDeclaration(options) {
|
|
290
116
|
readViewerSnapshot(options);
|
|
@@ -302,27 +128,29 @@ export function readArtifactDeclaration(options) {
|
|
|
302
128
|
workspaceRoot: options.workspaceRoot,
|
|
303
129
|
path: ARTIFACT_MANIFEST_PATH,
|
|
304
130
|
});
|
|
305
|
-
const parsed =
|
|
306
|
-
if (
|
|
307
|
-
return parsed;
|
|
131
|
+
const parsed = parseArtifactManifestContract(readBlob(options.workspaceRoot, fileEntry.oid, fileEntry.size));
|
|
132
|
+
if (!parsed.ok) {
|
|
133
|
+
return invalidArtifact(parsed.error.code, parsed.error.message);
|
|
308
134
|
}
|
|
309
|
-
const runtime =
|
|
135
|
+
const runtime = validateCommittedArtifact({
|
|
310
136
|
workspaceRoot: options.workspaceRoot,
|
|
311
|
-
|
|
137
|
+
runtime: parsed.declaration.runtime,
|
|
312
138
|
});
|
|
313
139
|
if ("status" in runtime) {
|
|
314
140
|
return runtime;
|
|
315
141
|
}
|
|
316
142
|
return {
|
|
317
143
|
status: "declared",
|
|
318
|
-
manifest: parsed,
|
|
144
|
+
manifest: parsed.declaration.manifest,
|
|
319
145
|
manifest_blob_oid: fileEntry.oid,
|
|
320
146
|
runtime,
|
|
321
147
|
projection: createDeclaredProjection({
|
|
322
|
-
manifest: parsed,
|
|
148
|
+
manifest: parsed.declaration.manifest,
|
|
323
149
|
manifestBlobOid: fileEntry.oid,
|
|
324
150
|
runtime,
|
|
325
|
-
...(options.previewBasePath === undefined
|
|
151
|
+
...(options.previewBasePath === undefined
|
|
152
|
+
? {}
|
|
153
|
+
: { previewBasePath: options.previewBasePath }),
|
|
326
154
|
}),
|
|
327
155
|
};
|
|
328
156
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
2
|
import type { ArtifactProjection, GetArtifactsResponse, HeartbeatArtifactPreviewDisposition, StartArtifactPreviewDisposition, StopArtifactPreviewDisposition } from "@tutti/shared/schemas/api";
|
|
3
|
+
import { type ArtifactPreviewProcessPlan, type ArtifactProcessRuntime } from "./process-boundary.js";
|
|
3
4
|
export type ArtifactPreviewCommandResult = {
|
|
4
5
|
artifact: ArtifactProjection;
|
|
5
6
|
};
|
|
@@ -20,42 +21,52 @@ export type ArtifactServerPreviewResponse = {
|
|
|
20
21
|
headers: Record<string, string>;
|
|
21
22
|
content: Buffer;
|
|
22
23
|
};
|
|
23
|
-
export type
|
|
24
|
-
command: "npm";
|
|
25
|
-
args: ["run", "artifact:preview"];
|
|
26
|
-
cwd: string;
|
|
27
|
-
env: NodeJS.ProcessEnv;
|
|
28
|
-
port: number;
|
|
29
|
-
};
|
|
24
|
+
export type ArtifactPreviewHttpMethod = "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS";
|
|
30
25
|
export type ArtifactPreviewManagerOptions = {
|
|
31
26
|
now?: () => Date;
|
|
32
27
|
baseEnv?: NodeJS.ProcessEnv;
|
|
33
28
|
spawnProcess?: (plan: ArtifactPreviewProcessPlan) => ChildProcessWithoutNullStreams;
|
|
29
|
+
terminateProcess?: (child: ChildProcessWithoutNullStreams) => Promise<void>;
|
|
34
30
|
fetchImpl?: typeof fetch;
|
|
35
|
-
allocatePort?: () => number
|
|
31
|
+
allocatePort?: () => number | Promise<number>;
|
|
32
|
+
createProcessRuntime?: () => ArtifactProcessRuntime;
|
|
33
|
+
cleanupProcessRuntime?: (runtime: ArtifactProcessRuntime) => void;
|
|
36
34
|
clockMs?: () => number;
|
|
37
35
|
setTimeoutFn?: typeof setTimeout;
|
|
38
36
|
clearTimeoutFn?: typeof clearTimeout;
|
|
39
37
|
onStatusChange?: () => void;
|
|
40
38
|
readyTimeoutMs?: number;
|
|
41
39
|
readyProbeIntervalMs?: number;
|
|
40
|
+
readyProbeRequestTimeoutMs?: number;
|
|
42
41
|
requestTimeoutMs?: number;
|
|
42
|
+
requestBodyLimitBytes?: number;
|
|
43
|
+
responseBodyLimitBytes?: number;
|
|
43
44
|
idleStopTtlMs?: number;
|
|
44
45
|
};
|
|
46
|
+
export declare const ARTIFACT_PREVIEW_REQUEST_BODY_LIMIT_BYTES: number;
|
|
47
|
+
export declare const ARTIFACT_PREVIEW_RESPONSE_BODY_LIMIT_BYTES: number;
|
|
48
|
+
export declare function isArtifactPreviewHttpMethod(method: string): method is ArtifactPreviewHttpMethod;
|
|
45
49
|
export declare class ArtifactPreviewManager {
|
|
46
50
|
private readonly now;
|
|
47
51
|
private readonly baseEnv;
|
|
48
52
|
private readonly spawnProcess;
|
|
53
|
+
private readonly terminateProcess;
|
|
49
54
|
private readonly fetchImpl;
|
|
50
55
|
private readonly allocatePort;
|
|
56
|
+
private readonly createProcessRuntime;
|
|
57
|
+
private readonly cleanupProcessRuntime;
|
|
51
58
|
private readonly clockMs;
|
|
52
59
|
private readonly setTimeoutFn;
|
|
53
60
|
private readonly clearTimeoutFn;
|
|
54
61
|
private readonly onStatusChange;
|
|
55
62
|
private readonly readyTimeoutMs;
|
|
56
63
|
private readonly readyProbeIntervalMs;
|
|
64
|
+
private readonly readyProbeRequestTimeoutMs;
|
|
57
65
|
private readonly requestTimeoutMs;
|
|
66
|
+
private readonly requestBodyLimitBytes;
|
|
67
|
+
private readonly responseBodyLimitBytes;
|
|
58
68
|
private readonly idleStopTtlMs;
|
|
69
|
+
private readonly pendingTasks;
|
|
59
70
|
private state;
|
|
60
71
|
private nextInstanceSequence;
|
|
61
72
|
constructor(options?: ArtifactPreviewManagerOptions);
|
|
@@ -78,20 +89,28 @@ export declare class ArtifactPreviewManager {
|
|
|
78
89
|
fetchServerPreview(options: {
|
|
79
90
|
workspaceRoot: string;
|
|
80
91
|
path: string | undefined;
|
|
92
|
+
method: ArtifactPreviewHttpMethod;
|
|
81
93
|
query?: string;
|
|
82
94
|
headers?: Record<string, string | undefined>;
|
|
95
|
+
body?: Buffer;
|
|
83
96
|
}): Promise<ArtifactServerPreviewResponse>;
|
|
84
97
|
sweepIdle(): void;
|
|
85
|
-
stopAll(): void
|
|
98
|
+
stopAll(): Promise<void>;
|
|
86
99
|
private startProcess;
|
|
100
|
+
private launchProcess;
|
|
87
101
|
private attachProcessListeners;
|
|
88
102
|
private waitUntilReady;
|
|
103
|
+
private probeReadyEntry;
|
|
89
104
|
private recordHeartbeat;
|
|
90
105
|
private scheduleIdleSweep;
|
|
91
106
|
private stopStaleRuntime;
|
|
92
107
|
private stopCurrentProcess;
|
|
93
108
|
private failState;
|
|
109
|
+
private disposeStateProcess;
|
|
110
|
+
private cleanupStateRuntime;
|
|
111
|
+
private trackTask;
|
|
94
112
|
private nextInstanceId;
|
|
95
113
|
}
|
|
114
|
+
export type { ArtifactPreviewProcessPlan } from "./process-boundary.js";
|
|
96
115
|
export declare const ARTIFACT_PREVIEW_DEFAULT_BASE_PATH: "/artifacts/preview";
|
|
97
116
|
//# sourceMappingURL=preview-runtime.d.ts.map
|