manifest 7.0.0 → 7.2.0
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/CONTRACT.md +25 -1
- package/LICENSE +21 -0
- package/README.md +64 -19
- package/dist/bin.cjs +410 -0
- package/dist/bin.d.cts +1 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +383 -0
- package/dist/chunk-GC5H22R2.js +495 -0
- package/dist/{chunk-DP2C4E42.js → chunk-NHEF7GUP.js} +126 -348
- package/dist/index.cjs +264 -24
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -2
- package/dist/register.cjs +264 -24
- package/dist/register.js +2 -1
- package/docs/guide.md +70 -10
- package/docs/sdk-flow-diagram.png +0 -0
- package/package.json +8 -3
package/CONTRACT.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
The SDK talks to the configured Manifest API using `Authorization: Bearer <project key>` and `User-Agent: mnfst-node/<version>`.
|
|
4
4
|
|
|
5
|
+
## Handshake
|
|
6
|
+
|
|
7
|
+
`POST /v1/hello` announces an install — `{"runtime":"node-22.0.0"}` — authorized with the same bearer key. A `200` confirms the key; `401`, or a `403` whose body is not `{"error":"project_disabled"}`, rejects it. The `200` body may carry project and activity context, all optional:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{"project": {"name": "Find Concierge"}, "requests": 12}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`manifest doctor` reads `project` (also accepting `projectName`, `project_name` or a bare `name`) and `requests` (also accepting `requestCount`, `requestsCount`, `requests_count`, `request_count`, or an object with `total`) to print the project and to warn when no request has arrived yet. Missing fields simply hide those lines.
|
|
14
|
+
|
|
15
|
+
A capture may carry a top-level `"synthetic": true`, which `manifest doctor --send-test` sets. Synthetic captures exist only to prove the pipeline end to end and must be excluded from statistics.
|
|
16
|
+
|
|
5
17
|
## Capture
|
|
6
18
|
|
|
7
19
|
`POST /v1/heal` receives:
|
|
@@ -21,7 +33,7 @@ A successful heal response may contain `status: patched|unverified`, `healAttemp
|
|
|
21
33
|
|
|
22
34
|
## Apply
|
|
23
35
|
|
|
24
|
-
A healed URL replaces the URL only within the original origin. Headers set or replace case-insensitively; null removes a header. Content length is recalculated. Objects merge using the server's healed body as the authoritative copy of fields sent to the server; withheld local credential fields are restored. Non-object JSON replaces the body. Form-urlencoded retries use the original encoding and are re-encoded from the parsed structure, so repeated keys return as indexed keys; a non-object healed body is not retried for them. Incomplete or malformed request captures and incomplete error captures are not retried.
|
|
36
|
+
A healed URL replaces the URL only within the original origin. Headers set or replace case-insensitively; null removes a header. Content length is recalculated. Objects merge using the server's healed body as the authoritative copy of fields sent to the server; withheld local credential fields are restored. Non-object JSON replaces the body. Form-urlencoded retries use the original encoding and are re-encoded from the parsed structure, so repeated keys return as indexed keys; a non-object healed body is not retried for them. A healed body that merges to nothing is sent as no body at all: GET, HEAD, DELETE and OPTIONS retry bodyless, and any other method is not retried. GET and HEAD never carry a body on a retry. Incomplete or malformed request captures and incomplete error captures are not retried.
|
|
25
37
|
|
|
26
38
|
Each captured failure permits one retry. A retry response, including another failure, is returned to the caller. A transport failure returns the original response. Successful response streams are not eagerly consumed.
|
|
27
39
|
|
|
@@ -48,3 +60,15 @@ Each captured failure permits one retry. A retry response, including another fai
|
|
|
48
60
|
HTTP status must be 200–599. Transport failures use a fixed message without exception prose. HTTP status zero is not a wire status. Transport failures and unattempted retries are inconclusive evidence; neither can verify or invalidate a patch. The server determines the verdict from the raw evidence, with the first accepted report winning.
|
|
49
61
|
|
|
50
62
|
Reports are best effort, bounded, and observable through Node warnings with code `MNFST`. The SDK sends the failed retry's raw body so the app can distinguish recurrence from a newly revealed issue. It does not assert `succeeded` or `failed` itself.
|
|
63
|
+
|
|
64
|
+
## Tracked requests
|
|
65
|
+
|
|
66
|
+
Every call the SDK sees and does not send to `POST /v1/heal`, whatever its status (2xx, 3xx, 401, 402, 403, 429, 5xx, and a 4xx that is not sent for healing: after a redirect, while the project is disabled, or while all eight heal slots are busy), is recorded and sent in batches to `POST /v1/requests`:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{"requests":[{"traceId":"a-uuid","method":"POST","url":"https://example.com/orders","statusCode":200,"responseTimeMs":84,"occurredAt":"2026-09-23T10:14:07.512Z"}]}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Metadata only. The URL carries scheme, host, port and path: no query string, userinfo or fragment. No headers and no request or response body are sent, and a response body is never read to record a call. A call sent to `/v1/heal` is not also tracked.
|
|
73
|
+
|
|
74
|
+
Recording is an in-memory append and never delays or fails the caller's request. A batch is sent when 500 calls are queued or every five seconds, at most once per second, one send at a time, up to 500 calls per request, with a five-second deadline. At most 5,000 calls are buffered; newer calls are dropped past that. A network error, timeout, 429 or 5xx is retried once, then the batch is dropped; any other answer, including 404 from a server without the route, drops it. HTTP 403 with `{"error":"project_disabled"}` suspends sending for five minutes, as for healing. Methods are upper-cased; a record whose method exceeds 16 characters or whose URL exceeds 4,096 is not sent. Buffered calls are sent once when the event loop drains (`beforeExit`), for at most two seconds: a send still in flight then is aborted and the rest is dropped, so a script never hangs on an unreachable server. A process killed or frozen first (serverless) can lose them.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MNFST, Inc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,10 +1,50 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
1
5
|
# Manifest for Node.js
|
|
2
6
|
|
|
7
|
+
**Turn 🔴 4xx API errors into 🟢 2xx in real time.**
|
|
8
|
+
|
|
3
9
|
[](https://github.com/mnfst/manifest-node/actions/workflows/ci.yml)
|
|
4
10
|
[](https://www.npmjs.com/package/manifest)
|
|
5
11
|
[](https://www.npmjs.com/package/manifest)
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## What is Manifest
|
|
18
|
+
|
|
19
|
+
Manifest is a self-healing layer that fixes and retries failed API requests on the fly.
|
|
20
|
+
|
|
21
|
+
* 🎯 **Fix failures automatically** before they impact your users.
|
|
22
|
+
* 🔔 **Get notified of root causes** so you can fix them permanently.
|
|
23
|
+
* 🔌 **Works across your stack** with internal APIs, external services, and agent tools.
|
|
24
|
+
|
|
25
|
+
## How it works
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
29
|
+
Every call your app makes is reported to Manifest as metadata only (method, URL without its query string, status and timing), in the background. A failure Manifest can heal is sent in full, so it can be repaired. [What is sent](docs/guide.md#data-sent-to-manifest).
|
|
30
|
+
|
|
31
|
+
## Prerequisites
|
|
32
|
+
|
|
33
|
+
- <a href="https://nodejs.org/en/download/" target="_blank">Node.js 22</a> or higher
|
|
34
|
+
|
|
35
|
+
## Get started
|
|
36
|
+
|
|
37
|
+
### Start with your agent
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
"Install Manifest in this app: https://dashboard.manifest.build/prompt-node.md"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
[Read the prompt →](https://dashboard.manifest.build/prompt-node.md)
|
|
44
|
+
|
|
45
|
+
The prompt adds the one-line install to your entry file and stops to let you paste your key.
|
|
46
|
+
|
|
47
|
+
### Start with code
|
|
8
48
|
|
|
9
49
|
```sh
|
|
10
50
|
npm install manifest
|
|
@@ -13,31 +53,40 @@ npm install manifest
|
|
|
13
53
|
```js
|
|
14
54
|
import { manifest } from 'manifest';
|
|
15
55
|
|
|
16
|
-
manifest();
|
|
56
|
+
manifest(); // Once, at startup.
|
|
17
57
|
// Keep making your API calls as usual.
|
|
18
58
|
```
|
|
19
59
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-

|
|
60
|
+
TypeScript, ESM and CommonJS. Zero dependencies.
|
|
23
61
|
|
|
24
62
|
## Setup
|
|
25
63
|
|
|
26
|
-
1. Create a project in your Manifest dashboard and copy its project key.
|
|
27
|
-
2.
|
|
28
|
-
3. Set the key:
|
|
64
|
+
1. Create a project in your [Manifest dashboard](https://dashboard.manifest.build) and copy its project key.
|
|
65
|
+
2. Set the key as an environment variable:
|
|
29
66
|
|
|
30
67
|
```sh
|
|
31
68
|
export MNFST_KEY='your-project-key'
|
|
32
69
|
```
|
|
33
70
|
|
|
34
|
-
Call `manifest()` before any
|
|
71
|
+
Call `manifest()` from the first import of the file that starts your app, before any client is constructed. Some clients keep the `fetch` they saw when they were built, and a client built at import time runs before your call. Where that happens, or where the start command is not yours to change, preload the SDK instead:
|
|
72
|
+
|
|
73
|
+
```sh
|
|
74
|
+
node -r manifest/register app.js
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Self-healing is enabled by default in your project settings.
|
|
78
|
+
|
|
79
|
+
Verify the install from your project directory:
|
|
35
80
|
|
|
36
81
|
```sh
|
|
37
|
-
|
|
82
|
+
npx manifest doctor
|
|
38
83
|
```
|
|
39
84
|
|
|
40
|
-
|
|
85
|
+
It resolves the installed SDK version, masks and validates the key against the handshake endpoint, checks that Manifest loads before your app, and prints the runtime coverage.
|
|
86
|
+
|
|
87
|
+
## Try it
|
|
88
|
+
|
|
89
|
+
Send a request that would normally fail. Manifest catches it, repairs it, and retries:
|
|
41
90
|
|
|
42
91
|
```js
|
|
43
92
|
import { manifest } from 'manifest';
|
|
@@ -51,17 +100,13 @@ manifest({
|
|
|
51
100
|
const res = await fetch('https://api.example.com/orders', {
|
|
52
101
|
method: 'POST',
|
|
53
102
|
headers: { 'content-type': 'application/json' },
|
|
54
|
-
body: JSON.stringify({ limit: 500 }),
|
|
103
|
+
body: JSON.stringify({ limit: 500 }), // Invalid? Manifest fixes it and retries.
|
|
55
104
|
});
|
|
105
|
+
console.log(res.status); // See the 200 OK response.
|
|
56
106
|
```
|
|
57
107
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- **Retries repeat side effects.** Use idempotency keys on non-idempotent calls.
|
|
61
|
-
- **A heal adds up to 60 s** to a failed request. Successful requests are untouched.
|
|
62
|
-
- **Not intercepted:** browsers, HTTP/2, and directly imported `undici`.
|
|
63
|
-
- **Privacy.** Failed URLs, headers, JSON or form-urlencoded bodies, and error responses are sent to Manifest. Known credentials are masked, but nested secrets and business data are not. Enable it only for traffic you allow Manifest to process.
|
|
108
|
+
Check your [Manifest dashboard](https://dashboard.manifest.build) to see all repairs and insights.
|
|
64
109
|
|
|
65
110
|
## More
|
|
66
111
|
|
|
67
|
-
[Configuration, limits & development](docs/guide.md) · [API contract](CONTRACT.md) · [Python SDK](https://github.com/mnfst/manifest-python)
|
|
112
|
+
[Configuration, limits & development](docs/guide.md) · [API contract](CONTRACT.md) · [Python SDK](https://github.com/mnfst/manifest-python) · [Website](https://manifest.build)
|
package/dist/bin.cjs
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli.ts
|
|
27
|
+
var import_node_module = require("module");
|
|
28
|
+
var import_node_fs = require("fs");
|
|
29
|
+
var import_promises = require("fs/promises");
|
|
30
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
31
|
+
|
|
32
|
+
// src/wire.ts
|
|
33
|
+
var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
34
|
+
|
|
35
|
+
// src/api.ts
|
|
36
|
+
var VERSION = "7.2.0";
|
|
37
|
+
|
|
38
|
+
// src/cli.ts
|
|
39
|
+
var DEFAULT_URL = "https://api.manifest.build";
|
|
40
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
41
|
+
var symbols = {
|
|
42
|
+
ok: "\u2705",
|
|
43
|
+
fail: "\u274C",
|
|
44
|
+
warn: "\u26A0\uFE0F",
|
|
45
|
+
skip: "\u2796"
|
|
46
|
+
};
|
|
47
|
+
function maskKey(key) {
|
|
48
|
+
const tail = key.slice(-4);
|
|
49
|
+
if (key.length <= tail.length + 2) return "\u2026";
|
|
50
|
+
const prefix = /^(?:mnfst(?:_[a-z]+)*_)/i.exec(key)?.[0] ?? key.slice(0, 4);
|
|
51
|
+
const head = prefix.length + tail.length < key.length ? prefix : "";
|
|
52
|
+
return `${head}\u2026${tail}`;
|
|
53
|
+
}
|
|
54
|
+
function normalizeBase(raw) {
|
|
55
|
+
try {
|
|
56
|
+
const url = new URL(raw);
|
|
57
|
+
if (!["https:", "http:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
58
|
+
return null;
|
|
59
|
+
if (!url.pathname.endsWith("/")) url.pathname += "/";
|
|
60
|
+
return url.toString();
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function readJson(response) {
|
|
66
|
+
try {
|
|
67
|
+
const text = await response.text();
|
|
68
|
+
return text.length > 1e6 ? null : JSON.parse(text);
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function projectName(body) {
|
|
74
|
+
if (!isObject(body)) return void 0;
|
|
75
|
+
const candidate = body.project ?? body.projectName ?? body.project_name ?? body.name;
|
|
76
|
+
if (typeof candidate === "string" && candidate.trim())
|
|
77
|
+
return candidate.trim();
|
|
78
|
+
if (isObject(candidate) && typeof candidate.name === "string" && candidate.name.trim())
|
|
79
|
+
return candidate.name.trim();
|
|
80
|
+
return void 0;
|
|
81
|
+
}
|
|
82
|
+
function requestCount(body) {
|
|
83
|
+
if (!isObject(body)) return void 0;
|
|
84
|
+
const candidate = body.requests ?? body.requestCount ?? body.requestsCount ?? body.requests_count ?? body.request_count;
|
|
85
|
+
if (typeof candidate === "number") return candidate;
|
|
86
|
+
if (isObject(candidate) && typeof candidate.total === "number")
|
|
87
|
+
return candidate.total;
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
function headers(key) {
|
|
91
|
+
return {
|
|
92
|
+
authorization: `Bearer ${key}`,
|
|
93
|
+
"content-type": "application/json",
|
|
94
|
+
"user-agent": `mnfst-node/${VERSION}`
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function probe(url, key, doFetch) {
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
100
|
+
try {
|
|
101
|
+
const response = await doFetch(new URL("v1/hello", url), {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: headers(key),
|
|
104
|
+
// `probe` marks this a key check, not a boot: the server answers but
|
|
105
|
+
// records no install. Without it, running doctor from a laptop makes
|
|
106
|
+
// the dashboard claim the app is connected — while doctor is printing
|
|
107
|
+
// "manifest is not installed in this project" two lines above.
|
|
108
|
+
body: JSON.stringify({ probe: true }),
|
|
109
|
+
signal: controller.signal,
|
|
110
|
+
redirect: "error"
|
|
111
|
+
});
|
|
112
|
+
const body = await readJson(response);
|
|
113
|
+
if (response.status === 200)
|
|
114
|
+
return {
|
|
115
|
+
kind: "valid",
|
|
116
|
+
project: projectName(body),
|
|
117
|
+
requests: requestCount(body)
|
|
118
|
+
};
|
|
119
|
+
if (response.status === 401)
|
|
120
|
+
return { kind: "invalid", detail: "the key was rejected (401)" };
|
|
121
|
+
if (response.status === 403)
|
|
122
|
+
return {
|
|
123
|
+
kind: "invalid",
|
|
124
|
+
detail: isObject(body) && body.error === "project_disabled" ? "the key is valid but the project is disabled (403)" : "the key was rejected (403)"
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
kind: "error",
|
|
128
|
+
detail: `unexpected response (${response.status})`
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
const timedOut = error instanceof Error && error.name === "AbortError";
|
|
132
|
+
return {
|
|
133
|
+
kind: "error",
|
|
134
|
+
detail: `${timedOut ? "timed out" : "could not be reached"} at ${url}`
|
|
135
|
+
};
|
|
136
|
+
} finally {
|
|
137
|
+
clearTimeout(timer);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
function installedVersion(cwd) {
|
|
141
|
+
try {
|
|
142
|
+
const entry = (0, import_node_module.createRequire)(import_node_path.default.join(cwd, "package.json")).resolve(
|
|
143
|
+
"manifest"
|
|
144
|
+
);
|
|
145
|
+
let dir = import_node_path.default.dirname(entry);
|
|
146
|
+
while (true) {
|
|
147
|
+
const manifest = import_node_path.default.join(dir, "package.json");
|
|
148
|
+
if ((0, import_node_fs.existsSync)(manifest)) {
|
|
149
|
+
const pkg = JSON.parse((0, import_node_fs.readFileSync)(manifest, "utf8"));
|
|
150
|
+
if (pkg.name === "manifest" && pkg.version) return pkg.version;
|
|
151
|
+
}
|
|
152
|
+
const parent = import_node_path.default.dirname(dir);
|
|
153
|
+
if (parent === dir) return void 0;
|
|
154
|
+
dir = parent;
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
return void 0;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function nearestPackage(cwd) {
|
|
161
|
+
let dir = import_node_path.default.resolve(cwd);
|
|
162
|
+
while (true) {
|
|
163
|
+
const file = import_node_path.default.join(dir, "package.json");
|
|
164
|
+
if ((0, import_node_fs.existsSync)(file)) {
|
|
165
|
+
try {
|
|
166
|
+
return {
|
|
167
|
+
dir,
|
|
168
|
+
pkg: JSON.parse(await (0, import_promises.readFile)(file, "utf8"))
|
|
169
|
+
};
|
|
170
|
+
} catch {
|
|
171
|
+
return void 0;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const parent = import_node_path.default.dirname(dir);
|
|
175
|
+
if (parent === dir) return void 0;
|
|
176
|
+
dir = parent;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
var instrumentationFiles = [
|
|
180
|
+
"instrumentation.ts",
|
|
181
|
+
"instrumentation.js",
|
|
182
|
+
"instrumentation.mjs",
|
|
183
|
+
"src/instrumentation.ts",
|
|
184
|
+
"src/instrumentation.js",
|
|
185
|
+
"src/instrumentation.mjs"
|
|
186
|
+
];
|
|
187
|
+
async function loadCheck(cwd) {
|
|
188
|
+
const label = "Loads before your app";
|
|
189
|
+
const found = await nearestPackage(cwd);
|
|
190
|
+
if (!found)
|
|
191
|
+
return {
|
|
192
|
+
label,
|
|
193
|
+
status: "warn",
|
|
194
|
+
detail: "no package.json found; cannot tell how Manifest loads"
|
|
195
|
+
};
|
|
196
|
+
const scripts = isObject(found.pkg.scripts) ? found.pkg.scripts : {};
|
|
197
|
+
const values = Object.values(scripts).filter(
|
|
198
|
+
(value) => typeof value === "string"
|
|
199
|
+
);
|
|
200
|
+
if (values.some((value) => value.includes("manifest/register")))
|
|
201
|
+
return {
|
|
202
|
+
label,
|
|
203
|
+
status: "ok",
|
|
204
|
+
detail: "a script preloads manifest/register"
|
|
205
|
+
};
|
|
206
|
+
const dependencies = {
|
|
207
|
+
...isObject(found.pkg.dependencies) ? found.pkg.dependencies : {},
|
|
208
|
+
...isObject(found.pkg.devDependencies) ? found.pkg.devDependencies : {}
|
|
209
|
+
};
|
|
210
|
+
if ("next" in dependencies) {
|
|
211
|
+
for (const relative of instrumentationFiles) {
|
|
212
|
+
const file = import_node_path.default.join(found.dir, relative);
|
|
213
|
+
if (!(0, import_node_fs.existsSync)(file)) continue;
|
|
214
|
+
if (/manifest/.test(await (0, import_promises.readFile)(file, "utf8")))
|
|
215
|
+
return {
|
|
216
|
+
label,
|
|
217
|
+
status: "ok",
|
|
218
|
+
detail: `${relative} installs Manifest before the app runs`
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
label,
|
|
223
|
+
status: "fail",
|
|
224
|
+
detail: 'package.json "start" relies on Next.js with no NODE_OPTIONS and no instrumentation file \u2014 Manifest will not load'
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
const start = scripts.start;
|
|
228
|
+
if (typeof start === "string" && start.trim())
|
|
229
|
+
return {
|
|
230
|
+
label,
|
|
231
|
+
status: "warn",
|
|
232
|
+
detail: "cannot tell from here whether manifest() runs; check Requests received"
|
|
233
|
+
};
|
|
234
|
+
return {
|
|
235
|
+
label,
|
|
236
|
+
status: "warn",
|
|
237
|
+
detail: "no start script found; cannot confirm Manifest loads before your app"
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function sdkCheck(cwd, injected) {
|
|
241
|
+
const label = "SDK installed";
|
|
242
|
+
const version = injected ?? installedVersion(cwd);
|
|
243
|
+
if (!version)
|
|
244
|
+
return {
|
|
245
|
+
label,
|
|
246
|
+
status: "fail",
|
|
247
|
+
detail: "manifest is not installed in this project"
|
|
248
|
+
};
|
|
249
|
+
return { label, status: "ok", detail: `manifest ${version}` };
|
|
250
|
+
}
|
|
251
|
+
async function runDoctor(options = {}) {
|
|
252
|
+
const cwd = options.cwd ?? process.cwd();
|
|
253
|
+
const env = options.env ?? process.env;
|
|
254
|
+
const doFetch = options.fetch ?? globalThis.fetch;
|
|
255
|
+
const checks = [sdkCheck(cwd, options.sdkVersion)];
|
|
256
|
+
const rawUrl = options.url ?? env.MNFST_URL ?? DEFAULT_URL;
|
|
257
|
+
const url = normalizeBase(rawUrl);
|
|
258
|
+
const key = env.MNFST_KEY;
|
|
259
|
+
if (!key) {
|
|
260
|
+
checks.push({
|
|
261
|
+
label: "MNFST_KEY set",
|
|
262
|
+
status: "fail",
|
|
263
|
+
detail: "MNFST_KEY is not set"
|
|
264
|
+
});
|
|
265
|
+
} else {
|
|
266
|
+
checks.push({ label: "MNFST_KEY set", status: "ok", detail: maskKey(key) });
|
|
267
|
+
}
|
|
268
|
+
let requests;
|
|
269
|
+
if (!url) {
|
|
270
|
+
checks.push({
|
|
271
|
+
label: "Key valid",
|
|
272
|
+
status: "fail",
|
|
273
|
+
detail: `invalid Manifest URL: ${rawUrl}`
|
|
274
|
+
});
|
|
275
|
+
} else if (!key) {
|
|
276
|
+
checks.push({ label: "Key valid", status: "skip", detail: "" });
|
|
277
|
+
} else {
|
|
278
|
+
const result = await probe(url, key, doFetch);
|
|
279
|
+
if (result.kind === "valid") {
|
|
280
|
+
requests = result.requests;
|
|
281
|
+
checks.push({
|
|
282
|
+
label: "Key valid",
|
|
283
|
+
status: "ok",
|
|
284
|
+
detail: result.project ? `project "${result.project}"` : "the server accepted the key"
|
|
285
|
+
});
|
|
286
|
+
} else {
|
|
287
|
+
checks.push({
|
|
288
|
+
label: "Key valid",
|
|
289
|
+
status: "fail",
|
|
290
|
+
detail: result.detail
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
checks.push(await loadCheck(cwd));
|
|
295
|
+
if (requests !== void 0) {
|
|
296
|
+
checks.push({
|
|
297
|
+
label: "Requests received",
|
|
298
|
+
status: requests > 0 ? "ok" : "warn",
|
|
299
|
+
detail: requests > 0 ? `${requests} request${requests === 1 ? "" : "s"} received` : "no requests received yet"
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
return { checks, ok: checks.every((check) => check.status !== "fail") };
|
|
303
|
+
}
|
|
304
|
+
function wrap(text, width) {
|
|
305
|
+
if (!text) return [""];
|
|
306
|
+
const lines = [];
|
|
307
|
+
let line = "";
|
|
308
|
+
for (const word of text.split(" ")) {
|
|
309
|
+
if (!line) line = word;
|
|
310
|
+
else if (line.length + 1 + word.length <= width) line += ` ${word}`;
|
|
311
|
+
else {
|
|
312
|
+
lines.push(line);
|
|
313
|
+
line = word;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (line) lines.push(line);
|
|
317
|
+
return lines;
|
|
318
|
+
}
|
|
319
|
+
function render(report) {
|
|
320
|
+
const width = Math.max(
|
|
321
|
+
20,
|
|
322
|
+
...report.checks.map((check) => check.label.length)
|
|
323
|
+
);
|
|
324
|
+
const lines = [];
|
|
325
|
+
for (const check of report.checks) {
|
|
326
|
+
const left = ` ${symbols[check.status]} ${check.label.padEnd(width)}`;
|
|
327
|
+
const detail = wrap(check.detail, Math.max(24, 96 - left.length - 2));
|
|
328
|
+
lines.push(`${left} ${detail[0] ?? ""}`.trimEnd());
|
|
329
|
+
for (const extra of detail.slice(1))
|
|
330
|
+
lines.push(`${" ".repeat(left.length + 2)}${extra}`);
|
|
331
|
+
}
|
|
332
|
+
lines.push("");
|
|
333
|
+
lines.push("Runtime coverage");
|
|
334
|
+
lines.push(
|
|
335
|
+
" Node.js runtime fetch, http.request, https.request, http.get are patched"
|
|
336
|
+
);
|
|
337
|
+
lines.push(
|
|
338
|
+
" Edge runtime not supported \u2014 middleware.ts and Edge route handlers are never covered"
|
|
339
|
+
);
|
|
340
|
+
return `${lines.join("\n")}
|
|
341
|
+
`;
|
|
342
|
+
}
|
|
343
|
+
function parseArgs(argv) {
|
|
344
|
+
const args = { help: false, version: false };
|
|
345
|
+
for (let index = 0; index < argv.length; index++) {
|
|
346
|
+
const arg = argv[index];
|
|
347
|
+
if (arg === "-h" || arg === "--help") args.help = true;
|
|
348
|
+
else if (arg === "-v" || arg === "--version") args.version = true;
|
|
349
|
+
else if (arg === "--url") {
|
|
350
|
+
const value = argv[++index];
|
|
351
|
+
if (!value) throw new Error("--url needs a value");
|
|
352
|
+
args.url = value;
|
|
353
|
+
} else if (arg.startsWith("--url=")) args.url = arg.slice("--url=".length);
|
|
354
|
+
else if (arg.startsWith("-")) throw new Error(`unknown option: ${arg}`);
|
|
355
|
+
else if (args.command === void 0) args.command = arg;
|
|
356
|
+
else throw new Error(`unexpected argument: ${arg}`);
|
|
357
|
+
}
|
|
358
|
+
return args;
|
|
359
|
+
}
|
|
360
|
+
var HELP = `Usage: manifest <command> [options]
|
|
361
|
+
|
|
362
|
+
Commands
|
|
363
|
+
doctor Check that this project is wired up to Manifest
|
|
364
|
+
|
|
365
|
+
Options
|
|
366
|
+
--url <url> Manifest API base URL (default: $MNFST_URL)
|
|
367
|
+
-h, --help Show this help
|
|
368
|
+
-v, --version Print the SDK version
|
|
369
|
+
|
|
370
|
+
Examples
|
|
371
|
+
npx manifest doctor
|
|
372
|
+
npx manifest doctor --url=https://api.manifest.build
|
|
373
|
+
`;
|
|
374
|
+
async function main(argv = process.argv.slice(2)) {
|
|
375
|
+
let args;
|
|
376
|
+
try {
|
|
377
|
+
args = parseArgs(argv);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
380
|
+
console.error(HELP);
|
|
381
|
+
return 1;
|
|
382
|
+
}
|
|
383
|
+
if (args.version) {
|
|
384
|
+
console.log(VERSION);
|
|
385
|
+
return 0;
|
|
386
|
+
}
|
|
387
|
+
if (args.help || !args.command) {
|
|
388
|
+
console.log(HELP);
|
|
389
|
+
return 0;
|
|
390
|
+
}
|
|
391
|
+
if (args.command !== "doctor") {
|
|
392
|
+
console.error(`Unknown command: ${args.command}`);
|
|
393
|
+
console.error(HELP);
|
|
394
|
+
return 1;
|
|
395
|
+
}
|
|
396
|
+
const report = await runDoctor({ url: args.url });
|
|
397
|
+
process.stdout.write(render(report));
|
|
398
|
+
return report.ok ? 0 : 1;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/bin.ts
|
|
402
|
+
main().then(
|
|
403
|
+
(code) => {
|
|
404
|
+
process.exitCode = code;
|
|
405
|
+
},
|
|
406
|
+
(error) => {
|
|
407
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
408
|
+
process.exitCode = 1;
|
|
409
|
+
}
|
|
410
|
+
);
|
package/dist/bin.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|