manifest 6.0.0 → 7.1.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 +14 -2
- package/LICENSE +21 -0
- package/README.md +68 -48
- 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-4OVLT3PO.js +358 -0
- package/dist/chunk-AR23I2QK.js +440 -0
- package/dist/index.cjs +377 -43
- package/dist/index.d.cts +2 -6
- package/dist/index.d.ts +2 -6
- package/dist/index.js +6 -439
- package/dist/register.cjs +791 -0
- package/dist/register.d.cts +2 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +7 -0
- package/docs/github-sdk.png +0 -0
- package/docs/guide.md +94 -20
- package/docs/healing-diagram.svg +549 -0
- package/docs/sdk-flow-diagram.png +0 -0
- package/package.json +20 -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:
|
|
@@ -15,13 +27,13 @@ The SDK talks to the configured Manifest API using `Authorization: Bearer <proje
|
|
|
15
27
|
}
|
|
16
28
|
```
|
|
17
29
|
|
|
18
|
-
Credential filtering and body limits are described in the README. Capture gates live in `runtime.ts`; the server owns repair policy.
|
|
30
|
+
Any 4xx response is captured except 401, 402, 403 and 429; those and every 5xx pass through untouched, because auth, billing, rate limiting and server faults are not repaired by editing the request. JSON bodies and `application/x-www-form-urlencoded` bodies are sent as structured JSON values. Credential filtering and body limits are described in the README. Capture gates live in `runtime.ts`; the server owns repair policy.
|
|
19
31
|
|
|
20
32
|
A successful heal response may contain `status: patched|unverified`, `healAttemptId`, `operations` and `healedRequest` with `url`, `headers` or `body`. Only these two statuses authorize a retry. No patch, malformed responses and unavailable service return the original error response. HTTP 403 with `{"error":"project_disabled"}` suppresses healing for five minutes.
|
|
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. Incomplete request
|
|
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
|
|
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,52 +1,93 @@
|
|
|
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)
|
|
10
|
+
[](https://www.npmjs.com/package/manifest)
|
|
11
|
+
[](https://www.npmjs.com/package/manifest)
|
|
4
12
|
|
|
5
|
-
|
|
13
|
+
</div>
|
|
6
14
|
|
|
7
|
-
|
|
8
|
-
import { manifest } from 'manifest';
|
|
15
|
+
---
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
// Keep making your API calls as usual.
|
|
12
|
-
```
|
|
17
|
+
## What is Manifest
|
|
13
18
|
|
|
14
|
-
|
|
19
|
+
Manifest is a self-healing layer that fixes and retries failed API requests on the fly.
|
|
15
20
|
|
|
16
|
-
|
|
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
|
+

|
|
17
28
|
|
|
18
|
-
|
|
29
|
+
## Prerequisites
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
- <a href="https://nodejs.org/en/download/" target="_blank">Node.js 22</a> or higher
|
|
32
|
+
|
|
33
|
+
## Get started
|
|
34
|
+
|
|
35
|
+
### Start with your agent
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
"Install Manifest in this app: https://dashboard.manifest.build/prompt-node.md"
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
[Read the prompt →](https://dashboard.manifest.build/prompt-node.md)
|
|
42
|
+
|
|
43
|
+
The prompt adds the one-line install to your entry file and stops to let you paste your key.
|
|
44
|
+
|
|
45
|
+
### Start with code
|
|
21
46
|
|
|
22
47
|
```sh
|
|
23
48
|
npm install manifest
|
|
24
49
|
```
|
|
25
50
|
|
|
26
|
-
|
|
51
|
+
```js
|
|
52
|
+
import { manifest } from 'manifest';
|
|
27
53
|
|
|
28
|
-
|
|
54
|
+
manifest(); // Once, at startup.
|
|
55
|
+
// Keep making your API calls as usual.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
TypeScript, ESM and CommonJS. Zero dependencies.
|
|
59
|
+
|
|
60
|
+
## Setup
|
|
29
61
|
|
|
30
|
-
Create a project in your Manifest dashboard and copy
|
|
62
|
+
1. Create a project in your [Manifest dashboard](https://dashboard.manifest.build) and copy its project key.
|
|
63
|
+
2. Set the key as an environment variable:
|
|
31
64
|
|
|
32
65
|
```sh
|
|
33
66
|
export MNFST_KEY='your-project-key'
|
|
34
67
|
```
|
|
35
68
|
|
|
36
|
-
|
|
69
|
+
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:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
node -r manifest/register app.js
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Self-healing is enabled by default in your project settings.
|
|
76
|
+
|
|
77
|
+
Verify the install from your project directory:
|
|
37
78
|
|
|
38
79
|
```sh
|
|
39
|
-
|
|
80
|
+
npx manifest doctor
|
|
40
81
|
```
|
|
41
82
|
|
|
42
|
-
|
|
83
|
+
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.
|
|
43
84
|
|
|
44
|
-
|
|
85
|
+
## Try it
|
|
45
86
|
|
|
46
|
-
|
|
87
|
+
Send a request that would normally fail. Manifest catches it, repairs it, and retries:
|
|
47
88
|
|
|
48
89
|
```js
|
|
49
|
-
import { manifest
|
|
90
|
+
import { manifest } from 'manifest';
|
|
50
91
|
|
|
51
92
|
manifest({
|
|
52
93
|
onHeal(event) {
|
|
@@ -54,37 +95,16 @@ manifest({
|
|
|
54
95
|
},
|
|
55
96
|
});
|
|
56
97
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
console.log(response.status, await response.text());
|
|
64
|
-
} finally {
|
|
65
|
-
await flush({ timeoutMs: 5000 });
|
|
66
|
-
}
|
|
98
|
+
const res = await fetch('https://api.example.com/orders', {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: { 'content-type': 'application/json' },
|
|
101
|
+
body: JSON.stringify({ limit: 500 }), // Invalid? Manifest fixes it and retries.
|
|
102
|
+
});
|
|
103
|
+
console.log(res.status); // See the 200 OK response.
|
|
67
104
|
```
|
|
68
105
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
CommonJS uses `const { manifest, flush } = require('manifest')`.
|
|
72
|
-
|
|
73
|
-
## Check that it works
|
|
74
|
-
|
|
75
|
-
Send a JSON request that your test API rejects with **400, 404 or 422**. Check the failure in your project's dashboard and the `onHeal` callback for the repair result. A successful request alone does not contact Manifest. `flush()` lets a short script wait for outcome reports before exiting.
|
|
76
|
-
|
|
77
|
-
## What to expect
|
|
78
|
-
|
|
79
|
-
- **One retry.** Manifest returns a repair; the SDK sends the corrected request directly to your API.
|
|
80
|
-
- **Original error if healing is unavailable.** A heal call can add up to 60 seconds. If a retry returns an HTTP response, that response reaches your application.
|
|
81
|
-
- **Built-in fetch.** Browser JavaScript, default Axios, `node:http` and separately imported fetch implementations are not intercepted.
|
|
82
|
-
- **Retry semantics still matter.** Use idempotency keys where needed; a repeated request can repeat side effects.
|
|
83
|
-
|
|
84
|
-
## Privacy
|
|
85
|
-
|
|
86
|
-
Manifest receives failed request URLs, headers, JSON bodies and error responses. Known credential fields are masked or withheld, but nested secrets, prompts and business data can still be sent. Enable it only for traffic you permit your Manifest server to process and store.
|
|
106
|
+
Check your [Manifest dashboard](https://dashboard.manifest.build) to see all repairs and insights.
|
|
87
107
|
|
|
88
108
|
## More
|
|
89
109
|
|
|
90
|
-
[Configuration, limits & development](docs/guide.md) · [API contract](CONTRACT.md) · [Python SDK](https://github.com/mnfst/manifest-python)
|
|
110
|
+
[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.1.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
|