@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.
Files changed (55) hide show
  1. package/dist/artifacts/candidate-validator.d.ts +39 -0
  2. package/dist/artifacts/candidate-validator.js +512 -0
  3. package/dist/artifacts/index.d.ts +3 -1
  4. package/dist/artifacts/index.js +3 -1
  5. package/dist/artifacts/manifest-contract.d.ts +30 -0
  6. package/dist/artifacts/manifest-contract.js +214 -0
  7. package/dist/artifacts/manifest.d.ts +3 -12
  8. package/dist/artifacts/manifest.js +29 -201
  9. package/dist/artifacts/preview-runtime.d.ts +29 -10
  10. package/dist/artifacts/preview-runtime.js +400 -155
  11. package/dist/artifacts/process-boundary.d.ts +33 -0
  12. package/dist/artifacts/process-boundary.js +134 -0
  13. package/dist/control-plane/types.d.ts +7 -2
  14. package/dist/prompt-templates/index.d.ts +1 -1
  15. package/dist/prompt-templates/index.js +1 -0
  16. package/dist/provider-usage/index.js +1 -0
  17. package/dist/run-pipeline/artifact-applicability.d.ts +49 -0
  18. package/dist/run-pipeline/artifact-applicability.js +281 -0
  19. package/dist/run-pipeline/openai.d.ts +6 -0
  20. package/dist/run-pipeline/openai.js +72 -2
  21. package/dist/run-pipeline/promotion-reconcile.d.ts +9 -1
  22. package/dist/run-pipeline/promotion-reconcile.js +65 -11
  23. package/dist/server-shell/cli/launch.d.ts +2 -0
  24. package/dist/server-shell/cli/launch.js +7 -1
  25. package/dist/server-shell/http/host-tunnel.js +1 -1
  26. package/dist/server-shell/http/routes/project-api/artifacts-routes.js +63 -14
  27. package/dist/server-shell/http/routes/project-api/types.d.ts +2 -0
  28. package/dist/server-shell/local-console/project-service.d.ts +5 -0
  29. package/dist/server-shell/local-console/project-service.js +25 -0
  30. package/dist/server-shell/local-console/server.js +10 -0
  31. package/dist/workspace-ops/index.d.ts +1 -1
  32. package/dist/workspace-ops/index.js +1 -1
  33. package/dist/workspace-ops/run-workspaces.d.ts +1 -0
  34. package/dist/workspace-ops/run-workspaces.js +27 -0
  35. package/node_modules/@tutti/shared/dist/schemas/api/artifacts.d.ts +28 -8
  36. package/node_modules/@tutti/shared/dist/schemas/api/artifacts.js +34 -9
  37. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  38. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  39. package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +5 -1
  40. package/node_modules/@tutti/shared/dist/utils/redaction/index.d.ts +1 -1
  41. package/node_modules/@tutti/shared/dist/utils/redaction/index.js +14 -0
  42. package/package.json +1 -1
  43. package/prompts/README.md +1 -0
  44. package/prompts/artifacts/README.md +7 -0
  45. package/prompts/artifacts/applicability.md +41 -0
  46. package/prompts/prompt-flow-map.md +29 -26
  47. package/prompts/runs/README.md +3 -3
  48. package/prompts/runs/task-continuation.md +36 -4
  49. package/prompts/runs/task-retry.md +42 -6
  50. package/prompts/runs/task-run.md +36 -4
  51. package/web/assets/index-B7I1QwT_.js +69 -0
  52. package/web/assets/index-F-ouhuJ5.css +1 -0
  53. package/web/index.html +2 -2
  54. package/web/assets/index-Sqw_t67u.css +0 -1
  55. 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
- export declare const ARTIFACT_MANIFEST_PATH: "tutti.artifact.json";
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 { requireNormalizedViewerPath } from "../workspace-ops/viewer-paths.js";
7
- export const ARTIFACT_MANIFEST_PATH = "tutti.artifact.json";
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 entryRepoPath = options.rootPath === "" ? options.entryPath : posix.join(options.rootPath, options.entryPath);
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 { ok: true, entryRepoPath };
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 ?? false,
80
+ network: artifact.network,
201
81
  },
202
82
  preview,
203
83
  };
204
84
  }
205
- function parseArtifactManifest(buffer) {
206
- if (buffer.byteLength > MAX_ARTIFACT_MANIFEST_BYTES) {
207
- return invalidArtifact("too_large", "Artifact manifest is too large");
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: root.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 entryResult = ensureStaticEntry({
96
+ const entryError = ensureStaticEntry({
253
97
  workspaceRoot: options.workspaceRoot,
254
- rootPath: root.path,
255
- entryPath: entry.path,
98
+ entryRepoPath: runtime.entry_repo_path,
256
99
  });
257
- if (!entryResult.ok) {
258
- return entryResult.declaration;
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: cwd.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 = parseArtifactManifest(readBlob(options.workspaceRoot, fileEntry.oid, fileEntry.size));
306
- if ("status" in parsed) {
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 = validateArtifactManifest({
135
+ const runtime = validateCommittedArtifact({
310
136
  workspaceRoot: options.workspaceRoot,
311
- manifest: parsed,
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 ? {} : { previewBasePath: options.previewBasePath }),
151
+ ...(options.previewBasePath === undefined
152
+ ? {}
153
+ : { previewBasePath: options.previewBasePath }),
326
154
  }),
327
155
  };
328
156
  }
@@ -1,5 +1,6 @@
1
- import { type ChildProcessWithoutNullStreams } from "node:child_process";
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 ArtifactPreviewProcessPlan = {
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