@uppercut-labs/agent-native 0.0.0-stage → 1.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/LICENSE +21 -0
- package/README.md +164 -2
- package/agent/agent-prompt-short.md +1 -0
- package/agent/agent-prompt.md +26 -0
- package/bin/agent-native-site.mjs +269 -0
- package/bin/discover-routes.mjs +162 -0
- package/bin/verify-publication.mjs +317 -0
- package/dist/adapters/zod.d.ts +4 -0
- package/dist/adapters/zod.d.ts.map +1 -0
- package/dist/adapters/zod.js +20 -0
- package/dist/adapters/zod.js.map +1 -0
- package/dist/astro.d.ts +13 -0
- package/dist/astro.d.ts.map +1 -0
- package/dist/astro.js +41 -0
- package/dist/astro.js.map +1 -0
- package/dist/auth.d.ts +52 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +128 -0
- package/dist/auth.js.map +1 -0
- package/dist/browser.d.ts +47 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +219 -0
- package/dist/browser.js.map +1 -0
- package/dist/cli.d.ts +80 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +484 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/assert-never.d.ts +2 -0
- package/dist/core/assert-never.d.ts.map +1 -0
- package/dist/core/assert-never.js +5 -0
- package/dist/core/assert-never.js.map +1 -0
- package/dist/core/binding-internal.d.ts +10 -0
- package/dist/core/binding-internal.d.ts.map +1 -0
- package/dist/core/binding-internal.js +15 -0
- package/dist/core/binding-internal.js.map +1 -0
- package/dist/core/composition.d.ts +119 -0
- package/dist/core/composition.d.ts.map +1 -0
- package/dist/core/composition.js +545 -0
- package/dist/core/composition.js.map +1 -0
- package/dist/core/contracts.d.ts +55 -0
- package/dist/core/contracts.d.ts.map +1 -0
- package/dist/core/contracts.js +271 -0
- package/dist/core/contracts.js.map +1 -0
- package/dist/core/diagnostics.d.ts +14 -0
- package/dist/core/diagnostics.d.ts.map +1 -0
- package/dist/core/diagnostics.js +29 -0
- package/dist/core/diagnostics.js.map +1 -0
- package/dist/core/executor.d.ts +52 -0
- package/dist/core/executor.d.ts.map +1 -0
- package/dist/core/executor.js +98 -0
- package/dist/core/executor.js.map +1 -0
- package/dist/core/registry.d.ts +37 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +75 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/schema.d.ts +9 -0
- package/dist/core/schema.d.ts.map +1 -0
- package/dist/core/schema.js +36 -0
- package/dist/core/schema.js.map +1 -0
- package/dist/discovery.d.ts +16 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +23 -0
- package/dist/discovery.js.map +1 -0
- package/dist/doctor.d.ts +200 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +287 -0
- package/dist/doctor.js.map +1 -0
- package/dist/harness-codex.d.ts +17 -0
- package/dist/harness-codex.d.ts.map +1 -0
- package/dist/harness-codex.js +485 -0
- package/dist/harness-codex.js.map +1 -0
- package/dist/harness-mcp-bridge.d.ts +14 -0
- package/dist/harness-mcp-bridge.d.ts.map +1 -0
- package/dist/harness-mcp-bridge.js +159 -0
- package/dist/harness-mcp-bridge.js.map +1 -0
- package/dist/harness.d.ts +72 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.js +101 -0
- package/dist/harness.js.map +1 -0
- package/dist/http.d.ts +20 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +537 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +73 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +1039 -0
- package/dist/init.js.map +1 -0
- package/dist/mcp-apps.d.ts +9 -0
- package/dist/mcp-apps.d.ts.map +1 -0
- package/dist/mcp-apps.js +171 -0
- package/dist/mcp-apps.js.map +1 -0
- package/dist/mcp-core.d.ts +64 -0
- package/dist/mcp-core.d.ts.map +1 -0
- package/dist/mcp-core.js +358 -0
- package/dist/mcp-core.js.map +1 -0
- package/dist/mcp.d.ts +3 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +2 -0
- package/dist/mcp.js.map +1 -0
- package/dist/next-browser.d.ts +14 -0
- package/dist/next-browser.d.ts.map +1 -0
- package/dist/next-browser.js +14 -0
- package/dist/next-browser.js.map +1 -0
- package/dist/next.d.ts +24 -0
- package/dist/next.d.ts.map +1 -0
- package/dist/next.js +28 -0
- package/dist/next.js.map +1 -0
- package/dist/package-version.d.ts +2 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +3 -0
- package/dist/package-version.js.map +1 -0
- package/dist/publication.d.ts +115 -0
- package/dist/publication.d.ts.map +1 -0
- package/dist/publication.js +441 -0
- package/dist/publication.js.map +1 -0
- package/dist/registry.d.ts +3 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +2 -0
- package/dist/registry.js.map +1 -0
- package/docs/adopt-existing-site.md +189 -0
- package/docs/browser.md +70 -0
- package/docs/capabilities-and-bindings.md +141 -0
- package/docs/cli.md +88 -0
- package/docs/compatibility.md +32 -0
- package/docs/composition-and-versioning.md +113 -0
- package/docs/doctor-and-troubleshooting.md +58 -0
- package/docs/evidence/GHSA-ch52-4w7c-c8xp-2026-10-03.md +17 -0
- package/docs/evidence/GHSA-ch52-4w7c-c8xp-2026-10-10.md +30 -0
- package/docs/evidence/GHSA-ch52-4w7c-c8xp-2026-10-12.md +10 -0
- package/docs/evidence/UAN-002-house-style-review.md +29 -0
- package/docs/evidence/UAN-007-astro.md +43 -0
- package/docs/evidence/UAN-008-worker-sidecar.md +56 -0
- package/docs/evidence/UAN-009-init.md +43 -0
- package/docs/evidence/UAN-010-auth.md +17 -0
- package/docs/evidence/UAN-011-discovery.md +18 -0
- package/docs/evidence/UAN-012-mcp-apps.md +14 -0
- package/docs/evidence/UAN-013-astro-on-demand.md +47 -0
- package/docs/evidence/UAN-014-next.md +57 -0
- package/docs/evidence/UAN-015-node-hono.md +69 -0
- package/docs/evidence/UAN-016-composition.md +52 -0
- package/docs/evidence/UAN-017-version-migrations.md +50 -0
- package/docs/evidence/UAN-018-surface-overrides.md +30 -0
- package/docs/evidence/UAN-019-doctor.md +19 -0
- package/docs/evidence/UAN-020-negative-security-fixtures.md +61 -0
- package/docs/evidence/UAN-020-operational-failure-safety.md +49 -0
- package/docs/evidence/UAN-021-docs-coverage.md +30 -0
- package/docs/evidence/UAN-022-cross-platform-license-notices.md +39 -0
- package/docs/evidence/UAN-022-overhead-license-audit.md +233 -0
- package/docs/evidence/UAN-022-package-portability.md +54 -0
- package/docs/evidence/UAN-023-commercial-agent-hosts.md +36 -0
- package/docs/evidence/UAN-023-inspector-host-probe.md +37 -0
- package/docs/evidence/UAN-023-inspector-web-and-astro-adoption.md +44 -0
- package/docs/evidence/UAN-023-native-webmcp-chrome.md +44 -0
- package/docs/evidence/UAN-024-release-readiness-2026-10-10.md +33 -0
- package/docs/evidence/UAN-024-release-readiness.md +41 -0
- package/docs/evidence/codex-harness.md +56 -0
- package/docs/evidence/gate-2-domain-expansion.md +35 -0
- package/docs/evidence/recovery-and-selection-recipes-2026-10-15.md +63 -0
- package/docs/for-agents.md +50 -0
- package/docs/frameworks.md +62 -0
- package/docs/getting-started.md +109 -0
- package/docs/harness-codex.md +117 -0
- package/docs/harness-mcp-bridge.md +30 -0
- package/docs/http.md +80 -0
- package/docs/installation.md +95 -0
- package/docs/mcp-apps.md +57 -0
- package/docs/mcp.md +178 -0
- package/docs/next.md +73 -0
- package/docs/permissions-and-discovery.md +58 -0
- package/docs/programmatic-harness.md +88 -0
- package/docs/readability-frameworks.md +18 -0
- package/docs/reference.md +161 -0
- package/docs/release-handoff.md +41 -0
- package/docs/site-publication.md +85 -0
- package/docs/stability.md +53 -0
- package/docs/static-sites.md +141 -0
- package/docs/support-and-limitations.md +20 -0
- package/docs/surfaces.md +27 -0
- package/docs/upgrading-to-1.0.md +26 -0
- package/package.json +188 -4
- package/skill/agent-native/SKILL.md +54 -0
- package/skill/agent-native/references/adapter-decision.md +19 -0
- package/skill/agent-native/references/verification.md +25 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Devin Thomas
|
|
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,3 +1,165 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Uppercut Agent Native
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Uppercut Agent Native defines typed capability contracts once and serves them on local,
|
|
4
|
+
HTTP, CLI, MCP, Node/Hono, browser, and Astro static/on-demand surfaces. It is distributed as
|
|
5
|
+
`@uppercut-labs/agent-native` under the MIT license. See [installation](docs/installation.md)
|
|
6
|
+
for the first programmatic capability.
|
|
7
|
+
|
|
8
|
+
To expose a real operation from a working app, start with [adopt an existing site](docs/adopt-existing-site.md).
|
|
9
|
+
For a coding assistant, use [the portable skill or standalone prompt](docs/for-agents.md).
|
|
10
|
+
Node.js 22+ is required. Eleven entry points are stable under SemVer; browser, framework, init
|
|
11
|
+
and harness integrations are experimental. See [stability](docs/stability.md). The package
|
|
12
|
+
ships importable capability APIs and an app-owned CLI runner. The development branch adds an experimental [site publication CLI](docs/site-publication.md).
|
|
13
|
+
|
|
14
|
+
## 1.1.0 candidate
|
|
15
|
+
|
|
16
|
+
This branch builds a candidate archive for `@uppercut-labs/agent-native@1.1.0`. Install the archive from the passing production-fixture workflow run to try it before registry publication. [Site publication CLI](docs/site-publication.md) · [Eight native examples](docs/readability-frameworks.md).
|
|
17
|
+
|
|
18
|
+
## Current scope
|
|
19
|
+
|
|
20
|
+
UAN-001 established the strict TypeScript baseline, Zod adapter, JSON Schema projection checks,
|
|
21
|
+
and stable diagnostic observations. UAN-002 adds immutable capability definitions, explicit
|
|
22
|
+
runtime bindings, a checked registry, per-invocation authorization, and shared input/output
|
|
23
|
+
validation. E01 exercises shared album lookup and a static Astro site integration; E12 is a
|
|
24
|
+
shared unit-converter example; E05 covers local and HTTP execution through the CLI. UAN-008 adds E04, a locally verified Worker sidecar for the same E01 contract while the Astro site remains static.
|
|
25
|
+
|
|
26
|
+
UAN-009 adds a programmatic, plan-first retrofit for a narrowly recognized existing Astro static
|
|
27
|
+
site. It tracks owned files, rechecks the plan before applying, and protects human edits during
|
|
28
|
+
restoration. The E01 check installs a local tarball into a copied site and builds the retrofit.
|
|
29
|
+
See the [experimental init guide](docs/getting-started.md) for its limits; no package-level init
|
|
30
|
+
command exists.
|
|
31
|
+
|
|
32
|
+
UAN-010 adds trusted principal and durable grant contracts. E07 demonstrates a local persistent
|
|
33
|
+
grant store with the official MCP SDK bearer gate, two fixture tenants, repeated protected edits,
|
|
34
|
+
separate delete permission, restart persistence and revocation. See
|
|
35
|
+
[permissions and discovery](docs/permissions-and-discovery.md). The E07 identities and store are
|
|
36
|
+
test-only and refuse production mode.
|
|
37
|
+
|
|
38
|
+
UAN-017 adds independent major-version composition and reviewed migration policy. V1 and v2 can
|
|
39
|
+
coexist and be selected by exact identity; aliases always target one canonical major. Structural
|
|
40
|
+
reports cover represented schema/risk/access changes, while semantic review and lifecycle state are
|
|
41
|
+
explicit. See [composition and versioning](docs/composition-and-versioning.md) and the E08 fixture.
|
|
42
|
+
|
|
43
|
+
Run `npm run example:e01`, `npm run example:e08`, `npm run example:e12`, or `npm run example:e05` to build and pack the
|
|
44
|
+
package, install it independently into each example, and run its tests and runnable surface. E05
|
|
45
|
+
uses one registry in a pinned Node/Hono host for generated HTTP and MCP plus local/remote CLI. E01
|
|
46
|
+
also verifies a pre-existing multi-page static Astro fixture before and after integration. E04 checks a local workerd HTTP/OpenAPI roundtrip and official MCP client exchange; it does not deploy or configure DNS.
|
|
47
|
+
|
|
48
|
+
E02 verifies Astro 7.3.5 on-demand routes with the official Node standalone adapter. It keeps the
|
|
49
|
+
human catalog and unrelated pages prerendered while serving authenticated Agent Native HTTP and MCP
|
|
50
|
+
routes from one shared contract, binding, authorization policy, and executor path.
|
|
51
|
+
|
|
52
|
+
The root, `./contracts`, `./registry`, and `./executor` entrypoints use only portable core modules.
|
|
53
|
+
They do not import Zod, Node, DOM, framework, provider, or server modules. Optional peers are
|
|
54
|
+
installed by the host only for the adapters it imports; see [installation](docs/installation.md). `npm run check` compiles
|
|
55
|
+
and scans browser output against a fake server-secret sentinel. This is a boundary regression
|
|
56
|
+
check, not proof of compatibility with any real browser, client, or host.
|
|
57
|
+
|
|
58
|
+
Every execution requires an `AuthorizationPort`; there is no implicit allow-all path. The executor
|
|
59
|
+
checks input, authorization, handler, and output in that order. Runtime matching is exact, and
|
|
60
|
+
multiple matching bindings require a `bindingId`. Durable grants are available through the
|
|
61
|
+
application-owned `GrantStorePort`; production identity, persistence and resource policy remain
|
|
62
|
+
host responsibilities. See [the capability and binding guide](docs/capabilities-and-bindings.md)
|
|
63
|
+
for the frozen signatures and limitations.
|
|
64
|
+
|
|
65
|
+
## Programmatic harness contracts
|
|
66
|
+
|
|
67
|
+
The isolated `@uppercut-labs/agent-native/harness` export defines provider-neutral
|
|
68
|
+
sessions, turns, feature negotiation, bounded normalized events, and stable failures.
|
|
69
|
+
Harness control is a sibling of capability execution and preserves its authorization
|
|
70
|
+
boundary. The Node-only `/harness/mcp-bridge` export serves an application-supplied
|
|
71
|
+
MCP handler on loopback with managed cleanup. The isolated `/harness/codex` adapter
|
|
72
|
+
controls an installed Codex app-server using managed ChatGPT sign-in; its local
|
|
73
|
+
Luna two-turn/resume proof passed. Cursor adapters remain future work. See
|
|
74
|
+
[harness contracts](docs/programmatic-harness.md) and [Codex lifecycle](docs/harness-codex.md).
|
|
75
|
+
|
|
76
|
+
## HTTP and OpenAPI
|
|
77
|
+
|
|
78
|
+
The separate `@uppercut-labs/agent-native/http` entrypoint provides a framework-neutral Web
|
|
79
|
+
Request/Response handler. It exposes deterministic invocation routes for explicit public read
|
|
80
|
+
capabilities, including validated GET overrides, generates OpenAPI 3.1 from those contracts, and serves protocol health evidence.
|
|
81
|
+
Protected capability schemas and routes are omitted from this initial HTTP projection. See
|
|
82
|
+
[the HTTP guide](docs/http.md) for response mappings, body/deadline limits, and integration boundaries.
|
|
83
|
+
|
|
84
|
+
## Browser tools
|
|
85
|
+
|
|
86
|
+
The isolated `@uppercut-labs/agent-native/browser` entrypoint feature-detects the current WebMCP
|
|
87
|
+
document.modelContext API and owns registrations with an AbortSignal. It projects only definitions
|
|
88
|
+
with browser bindings; public reads are exposed by default, while protected tools require an explicit
|
|
89
|
+
projection policy and are still authorized on every call. Unit and example fixtures use a simulated
|
|
90
|
+
API. E01 and E06 also passed against native WebMCP in Chrome 155 with its opt-in `WebMCP` feature; no
|
|
91
|
+
shipping browser agent is claimed. See [the browser guide](docs/browser.md) and the
|
|
92
|
+
[native Chrome evidence](docs/evidence/UAN-023-native-webmcp-chrome.md).
|
|
93
|
+
|
|
94
|
+
## Astro sites
|
|
95
|
+
|
|
96
|
+
The isolated `@uppercut-labs/agent-native/astro` integration injects an application-owned browser
|
|
97
|
+
entry with Astro's page-script hook and validates the selected static or on-demand mode. It never
|
|
98
|
+
installs a server adapter. E01 remains wholly static; E02 uses application-owned Astro endpoints and
|
|
99
|
+
the official Node standalone adapter while retaining prerendered pages and navigation. See
|
|
100
|
+
[static-site guidance](docs/static-sites.md), [framework support](docs/frameworks.md), and
|
|
101
|
+
[UAN-007 evidence](docs/evidence/UAN-007-astro.md).
|
|
102
|
+
|
|
103
|
+
## Next.js App Router
|
|
104
|
+
|
|
105
|
+
The isolated `next` entrypoint adapts the existing HTTP and official MCP handlers to Next 16.3.8
|
|
106
|
+
Node-runtime route exports without importing Next into core or Astro consumers. Browser bootstrap
|
|
107
|
+
code lives at `next/browser` and resyncs explicitly from `usePathname()` while preserving a normal
|
|
108
|
+
browser fallback. E03 retains its pre-integration fixture and verifies a generated, locked
|
|
109
|
+
after-state. See [Next.js App Router](docs/next.md) and
|
|
110
|
+
[UAN-014 evidence](docs/evidence/UAN-014-next.md).
|
|
111
|
+
|
|
112
|
+
## Remote MCP
|
|
113
|
+
|
|
114
|
+
The isolated `@uppercut-labs/agent-native/mcp` entrypoint uses the official MCP server SDK over
|
|
115
|
+
Streamable HTTP. It exposes explicitly public read capabilities by default and can discover
|
|
116
|
+
protected capabilities only with an explicit callback and verified auth integration. It filters
|
|
117
|
+
discovery per request and reauthorizes every call through the shared executor. Results place the
|
|
118
|
+
canonical value under `structuredContent.result`. See [the MCP guide](docs/mcp.md) and
|
|
119
|
+
[fixture-tested compatibility](docs/compatibility.md). The fixture covers the official SDK client
|
|
120
|
+
and server packages at version 2.3.0 and the negotiated protocol version 2025-11-25; this is not a
|
|
121
|
+
claim that every commercial client or deployment host is supported.
|
|
122
|
+
|
|
123
|
+
## Generated CLI
|
|
124
|
+
|
|
125
|
+
The separate `@uppercut-labs/agent-native/cli` runner entrypoint builds help and typed field flags from registry schemas. Calls select `--mode local|remote` explicitly; remote calls also select a configured credential profile and use a bounded HTTP request. Invocations write one `uan.cli-result/v1` JSON envelope to stdout and redacted target/failure diagnostics to stderr. No package-level `bin` command ships yet; applications expose the runner with their own registry. See [the CLI guide](docs/cli.md) and the installed E05/E12 examples.
|
|
126
|
+
|
|
127
|
+
## Diagnostics
|
|
128
|
+
|
|
129
|
+
The `./doctor` entrypoint builds versioned findings, required-check profiles, read-only registry
|
|
130
|
+
inspection, and policy-filtered capability lists. E11 packs the package into a fault/repair lab;
|
|
131
|
+
its local profile reports only local evidence, and the default full profile remains incomplete
|
|
132
|
+
without an explicit loopback probe. See [doctor and troubleshooting](docs/doctor-and-troubleshooting.md).
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
Requires Node.js 22 or newer and npm.
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
npm ci
|
|
140
|
+
npm run format
|
|
141
|
+
npm run format:check
|
|
142
|
+
npm run lint
|
|
143
|
+
npm run typecheck
|
|
144
|
+
npm test
|
|
145
|
+
npm run build
|
|
146
|
+
npm run example:e01
|
|
147
|
+
npm run example:e02
|
|
148
|
+
npm run example:e12
|
|
149
|
+
npm run example:e05
|
|
150
|
+
npm run example:e07
|
|
151
|
+
npm run check:portability
|
|
152
|
+
npm run check
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
The schema spike checks album lookup and unit conversion input/output shapes with Zod 4, then
|
|
156
|
+
validates exported draft-2020-12 schemas independently with Ajv. JSON Schema projection throws for
|
|
157
|
+
transforms that cannot be represented faithfully. The official MCP server SDK is an optional peer required by the isolated MCP adapter; the
|
|
158
|
+
official MCP client SDK is pinned for the local protocol fixture only. See [UAN-008 sidecar evidence](docs/evidence/UAN-008-worker-sidecar.md) for host/origin controls, revision matching, and local-only limits.
|
|
159
|
+
|
|
160
|
+
## Status
|
|
161
|
+
|
|
162
|
+
Version `1.0.1` is released under MIT. Stable entry points follow semantic versioning; see [stability](docs/stability.md) and [upgrading from 0.1.x](docs/upgrading-to-1.0.md). UAN-002 local acceptance and limitations are in
|
|
163
|
+
[UAN-002 evidence](https://github.com/uppercut-labs/agent-native/blob/main/evidence/UAN-002.md); pinned tool versions and upstream references are in [VERSION-EVIDENCE.md](https://github.com/uppercut-labs/agent-native/blob/main/VERSION-EVIDENCE.md).
|
|
164
|
+
The current init acceptance and limits are in [UAN-009 evidence](docs/evidence/UAN-009-init.md).
|
|
165
|
+
The local trusted-auth and grant acceptance is in [UAN-010 evidence](docs/evidence/UAN-010-auth.md).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Read https://docs.uppercut-labs.com/agent-native/adopt-existing-site/ and expose one existing website operation to agents in this repository.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Agent Native — website integration agent prompt
|
|
2
|
+
|
|
3
|
+
**Use:** Paste the text below into your preferred coding agent alongside access to your website repository. Replace the bracketed task only if you have a specific operation in mind. The prompt works without a skill plugin or private Uppercut materials.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
I want to make **this existing website genuinely agent-native** using the published MIT-licensed `@uppercut-labs/agent-native` TypeScript library, not replace the website with a chatbot and not merely build a synthetic demo. Your task is to add a real, typed, permission-checked agent-callable capability built on an existing function or application operation.
|
|
8
|
+
|
|
9
|
+
My target operation, if specified: **[Describe one real operation; otherwise identify and implement the most useful existing public read.]**
|
|
10
|
+
|
|
11
|
+
Read the repository instructions, package manifest, lockfile, framework, routes, current human workflows, authentication boundaries and relevant source code before changing anything. Use the Agent Native integration skill if installed; if not, rely on official public docs: https://github.com/uppercut-labs/agent-native/blob/main/docs/installation.md and the existing-function retrofit at https://github.com/uppercut-labs/agent-native/tree/main/examples/e10-existing-functions-retrofit. Check the installed npm package's exports before assuming option names. The inspected candidate is `1.1.0` and requires Node.js 22+; do not silently upgrade the app's framework or optional peers.
|
|
12
|
+
|
|
13
|
+
Choose a minimal implementation that respects the site's actual architecture. Start with an **existing public read** unless my request specifically authorizes authenticated actions. Define a versioned capability with real input/output schemas, honest risk/access metadata, an explicit runtime binding to the existing domain function, and a checked registry. Use an application-owned authorization port on **every invocation**. Preserve current human UI, working APIs, records, routes and auth.
|
|
14
|
+
|
|
15
|
+
Identity namespace and name use lower-case slug segments separated by dots or hyphens,
|
|
16
|
+
such as `{ namespace: 'blog', name: 'list-posts', majorVersion: 1 }`.
|
|
17
|
+
|
|
18
|
+
Expose an actual useful agent surface, not just a local registry. If this website has a server, prefer HTTP/OpenAPI for public reads and, where useful, official MCP Streamable HTTP. If it is static, retain its static output and use a separately hosted server/Worker sidecar for remote tool calls, or explain precisely why a server cannot be provided within this work scope. If Next App Router, use its documented Node route adapter; if Astro, use its supported mode deliberately. Browser WebMCP is optional/experimental and requires normal UI fallback. There is **no** global `agent-native init` CLI, no automatic discovery of business operations, and no built-in production identity/grant service.
|
|
19
|
+
|
|
20
|
+
Never turn protected customer data, user-specific operations, writes or destructive actions into public tools. For protected MCP functionality, use the host's real verified identity, current scopes, application-persisted grants and per-record authorization; do not use a fixture store or unconditional allow-all. Don't place tokens or server-only SDKs in client bundles. Don't create paid services, change DNS, deploy, publish a package, change external permissions, or modify production records unless separately authorized.
|
|
21
|
+
|
|
22
|
+
**Execute the implementation and its local tests**, not just a plan. First write a short source-grounded integration outline. Then make minimal focused changes. Prove one real successful call on the chosen surface, at least one invalid-input rejection **before handler execution**, at least one denied/hidden path, and one existing human website flow still works. For MCP, use an actual compatible MCP client and inspect `structuredContent.result`; for HTTP, test the actual mounted route and method/schema. If a deployment or commercial host wasn't tested, label it *unverified*, not complete. Avoid fake green tests, invented commands and unused scaffolding.
|
|
23
|
+
|
|
24
|
+
Finish with the implemented capability name/version, real function reused, risk/access policy, chosen runtime and surface, file diffs, install/test commands, observed positive and negative outputs, known limits, needed owner-owned deployment/auth steps, and a rollback plan. If important access or an authorized external action is missing, complete independent safe work and precisely identify what remains blocked.
|
|
25
|
+
|
|
26
|
+
---
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
+
import { lstat, mkdir, readFile, realpath, rename, unlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
import { planPublication, safePublicationPath } from '@uppercut-labs/agent-native/publication';
|
|
7
|
+
import { discoverRoutes } from './discover-routes.mjs';
|
|
8
|
+
import { verifyProtocol, verifyPublication } from './verify-publication.mjs';
|
|
9
|
+
|
|
10
|
+
const help =
|
|
11
|
+
'agent-native-site discover --framework NAME --project DIR [--routes TABLE.json] | agent-native-site <preview|apply|restore-preview|restore|verify> --manifest FILE [--out EXISTING_DIR --state EXISTING_PRIVATE_DIR] [--approve DIGEST]';
|
|
12
|
+
const hash = (bytes) => createHash('sha256').update(bytes).digest('hex');
|
|
13
|
+
const fail = (code, detail) => {
|
|
14
|
+
throw Object.assign(new Error(code), { diagnostic: code, detail });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
async function optional(read) {
|
|
18
|
+
try {
|
|
19
|
+
return await read();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.code === 'ENOENT') return null;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function within(root, target) {
|
|
27
|
+
const relative = path.relative(root, target);
|
|
28
|
+
return (
|
|
29
|
+
relative === '' ||
|
|
30
|
+
(relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Reject symlinks in existing components, including a symlink at the leaf.
|
|
35
|
+
// The caller supplies a canonical root and relative names produced by the engine.
|
|
36
|
+
async function inspect(root, name) {
|
|
37
|
+
if (!safePublicationPath(name)) fail('unsafe-path', name);
|
|
38
|
+
const segments = name.split('/');
|
|
39
|
+
let location = root;
|
|
40
|
+
for (const [i, segment] of segments.entries()) {
|
|
41
|
+
location = path.join(location, segment);
|
|
42
|
+
const stat = await optional(() => lstat(location));
|
|
43
|
+
if (!stat) return null;
|
|
44
|
+
if (stat.isSymbolicLink() || (i === segments.length - 1 ? !stat.isFile() : !stat.isDirectory()))
|
|
45
|
+
fail('unsafe-path', name);
|
|
46
|
+
}
|
|
47
|
+
return readFile(location);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function readJournal(file, out) {
|
|
51
|
+
const stat = await optional(() => lstat(file));
|
|
52
|
+
if (!stat) return {};
|
|
53
|
+
if (!stat.isFile() || stat.isSymbolicLink()) fail('invalid-journal');
|
|
54
|
+
const record = JSON.parse(await readFile(file, 'utf8'));
|
|
55
|
+
if (
|
|
56
|
+
record.version !== 1 ||
|
|
57
|
+
record.out !== out ||
|
|
58
|
+
!record.files ||
|
|
59
|
+
Array.isArray(record.files) ||
|
|
60
|
+
typeof record.files !== 'object'
|
|
61
|
+
)
|
|
62
|
+
fail('invalid-journal');
|
|
63
|
+
for (const [name, digest] of Object.entries(record.files)) {
|
|
64
|
+
if (!safePublicationPath(name) || !/^[a-f0-9]{64}$/.test(digest)) fail('invalid-journal');
|
|
65
|
+
}
|
|
66
|
+
return record.files;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function snapshot(out, state, plan, restore) {
|
|
70
|
+
const journal = await readJournal(path.join(state, 'ownership.json'), out);
|
|
71
|
+
const assets = new Map((restore ? [] : plan.assets).map((a) => [a.path, a]));
|
|
72
|
+
const changes = [],
|
|
73
|
+
conflicts = [],
|
|
74
|
+
backup = new Map();
|
|
75
|
+
for (const name of [...new Set([...Object.keys(journal), ...assets.keys()])].sort()) {
|
|
76
|
+
const bytes = await inspect(out, name);
|
|
77
|
+
const before = bytes === null ? null : hash(bytes);
|
|
78
|
+
const owned = Object.hasOwn(journal, name);
|
|
79
|
+
if (owned ? before !== journal[name] : before !== null) {
|
|
80
|
+
conflicts.push(name);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const after = assets.get(name)?.digest ?? null;
|
|
84
|
+
if (after === before) continue;
|
|
85
|
+
backup.set(name, bytes);
|
|
86
|
+
changes.push({ path: name, before, after });
|
|
87
|
+
}
|
|
88
|
+
const digest = hash(
|
|
89
|
+
JSON.stringify([
|
|
90
|
+
restore ? 'restore' : 'apply',
|
|
91
|
+
plan.digest,
|
|
92
|
+
out,
|
|
93
|
+
state,
|
|
94
|
+
journal,
|
|
95
|
+
changes,
|
|
96
|
+
conflicts,
|
|
97
|
+
]),
|
|
98
|
+
);
|
|
99
|
+
return { digest, changes, conflicts, backup, assets };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function main(argv) {
|
|
103
|
+
const { values, positionals } = parseArgs({
|
|
104
|
+
args: argv,
|
|
105
|
+
allowPositionals: true,
|
|
106
|
+
options: {
|
|
107
|
+
manifest: { type: 'string' },
|
|
108
|
+
framework: { type: 'string' },
|
|
109
|
+
project: { type: 'string' },
|
|
110
|
+
routes: { type: 'string' },
|
|
111
|
+
out: { type: 'string' },
|
|
112
|
+
state: { type: 'string' },
|
|
113
|
+
approve: { type: 'string' },
|
|
114
|
+
help: { type: 'boolean', short: 'h' },
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
if (values.help || argv.length === 0) return { usage: help };
|
|
118
|
+
const [command] = positionals;
|
|
119
|
+
if (command === 'discover' && positionals.length === 1 && values.framework && values.project) {
|
|
120
|
+
return discoverRoutes(values.framework, values.project, values.routes);
|
|
121
|
+
}
|
|
122
|
+
if (
|
|
123
|
+
positionals.length !== 1 ||
|
|
124
|
+
!['preview', 'apply', 'restore-preview', 'restore', 'verify'].includes(command) ||
|
|
125
|
+
!values.manifest
|
|
126
|
+
)
|
|
127
|
+
fail('invalid-usage', help);
|
|
128
|
+
const manifestFile = await realpath(values.manifest);
|
|
129
|
+
const input = JSON.parse(await readFile(manifestFile, 'utf8'));
|
|
130
|
+
const restore = command === 'restore' || command === 'restore-preview';
|
|
131
|
+
const plan = await planPublication(input.config, input.candidates, { verifyProtocol });
|
|
132
|
+
if (!restore && plan.diagnostics.some((d) => d.severity === 'error'))
|
|
133
|
+
fail('invalid-publication', plan.diagnostics);
|
|
134
|
+
if (command === 'verify')
|
|
135
|
+
return verifyPublication(plan, input.config, input.verify, input.candidates);
|
|
136
|
+
if (!values.out || !values.state) fail('invalid-usage', help);
|
|
137
|
+
const [out, state] = await Promise.all([realpath(values.out), realpath(values.state)]);
|
|
138
|
+
if (
|
|
139
|
+
!(await lstat(out)).isDirectory() ||
|
|
140
|
+
!(await lstat(state)).isDirectory() ||
|
|
141
|
+
within(out, state) ||
|
|
142
|
+
within(state, out) ||
|
|
143
|
+
within(out, manifestFile)
|
|
144
|
+
)
|
|
145
|
+
fail('invalid-roots');
|
|
146
|
+
const preview = command === 'preview' || command === 'restore-preview';
|
|
147
|
+
if (preview) {
|
|
148
|
+
const { digest, changes, conflicts } = await snapshot(out, state, plan, restore);
|
|
149
|
+
return {
|
|
150
|
+
status: conflicts.length ? 'conflict' : 'preview',
|
|
151
|
+
digest,
|
|
152
|
+
changes,
|
|
153
|
+
conflicts,
|
|
154
|
+
manifest: plan.manifest,
|
|
155
|
+
diagnostics: plan.diagnostics,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
if (!values.approve) fail('approval-required');
|
|
159
|
+
const lock = path.join(state, 'publication.lock');
|
|
160
|
+
try {
|
|
161
|
+
await writeFile(lock, String(process.pid), { flag: 'wx', mode: 0o600 });
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (error.code === 'EEXIST') fail('publication-locked');
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
let releaseLock = true;
|
|
167
|
+
try {
|
|
168
|
+
const { digest, changes, conflicts, backup, assets } = await snapshot(
|
|
169
|
+
out,
|
|
170
|
+
state,
|
|
171
|
+
plan,
|
|
172
|
+
restore,
|
|
173
|
+
);
|
|
174
|
+
if (conflicts.length) fail('ownership-conflict', conflicts);
|
|
175
|
+
if (digest !== values.approve) fail('stale-approval');
|
|
176
|
+
if (!changes.length) return { status: 'no-op', digest };
|
|
177
|
+
const pending = path.join(state, 'pending.json');
|
|
178
|
+
const priorFiles = await readJournal(path.join(state, 'ownership.json'), out);
|
|
179
|
+
try {
|
|
180
|
+
await writeFile(
|
|
181
|
+
pending,
|
|
182
|
+
JSON.stringify({
|
|
183
|
+
version: 1,
|
|
184
|
+
out,
|
|
185
|
+
digest,
|
|
186
|
+
priorFiles,
|
|
187
|
+
changes: changes.map((change) => ({
|
|
188
|
+
...change,
|
|
189
|
+
original: backup.get(change.path)?.toString('base64') ?? null,
|
|
190
|
+
})),
|
|
191
|
+
}),
|
|
192
|
+
{ flag: 'wx', mode: 0o600, flush: true },
|
|
193
|
+
);
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (error.code === 'EEXIST') fail('recovery-required');
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
const applied = [];
|
|
199
|
+
const journal = path.join(state, 'ownership.json');
|
|
200
|
+
const tempJournal = path.join(state, `${randomUUID()}.tmp`);
|
|
201
|
+
try {
|
|
202
|
+
for (const change of changes) {
|
|
203
|
+
const dest = path.join(out, change.path);
|
|
204
|
+
const prior = await inspect(out, change.path);
|
|
205
|
+
if ((prior === null ? null : hash(prior)) !== change.before)
|
|
206
|
+
fail('stale-file', change.path);
|
|
207
|
+
const next = assets.get(change.path);
|
|
208
|
+
if (prior !== null) await unlink(dest);
|
|
209
|
+
applied.push(change);
|
|
210
|
+
if (next) {
|
|
211
|
+
await mkdir(path.dirname(dest), { recursive: true });
|
|
212
|
+
await writeFile(dest, next.content, { flag: 'wx' });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const files = Object.fromEntries(
|
|
216
|
+
[...assets]
|
|
217
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
218
|
+
.map(([name, asset]) => [name, asset.digest]),
|
|
219
|
+
);
|
|
220
|
+
await writeFile(
|
|
221
|
+
tempJournal,
|
|
222
|
+
JSON.stringify({ version: 1, out, files, transaction: digest }),
|
|
223
|
+
{
|
|
224
|
+
flag: 'wx',
|
|
225
|
+
mode: 0o600,
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
await rename(tempJournal, journal);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
await optional(() => unlink(tempJournal));
|
|
231
|
+
for (const change of applied.reverse()) {
|
|
232
|
+
try {
|
|
233
|
+
const current = await inspect(out, change.path);
|
|
234
|
+
if (current !== null) {
|
|
235
|
+
if (hash(current) !== change.after) fail('rollback-conflict', change.path);
|
|
236
|
+
await unlink(path.join(out, change.path));
|
|
237
|
+
}
|
|
238
|
+
const original = backup.get(change.path);
|
|
239
|
+
if (original !== null)
|
|
240
|
+
await writeFile(path.join(out, change.path), original, { flag: 'wx' });
|
|
241
|
+
} catch (recoveryError) {
|
|
242
|
+
releaseLock = false;
|
|
243
|
+
throw recoveryError;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
await unlink(pending);
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
await unlink(pending);
|
|
250
|
+
return { status: restore ? 'restored' : 'applied', digest, changes };
|
|
251
|
+
} finally {
|
|
252
|
+
if (releaseLock) await unlink(lock);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try {
|
|
257
|
+
const result = await main(process.argv.slice(2));
|
|
258
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
259
|
+
if (result.status === 'conflict' || result.status === 'failed') process.exitCode = 1;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
process.stdout.write(
|
|
262
|
+
`${JSON.stringify({
|
|
263
|
+
status: 'failed',
|
|
264
|
+
code: error.diagnostic ?? error.code ?? 'invalid-input',
|
|
265
|
+
detail: error.detail,
|
|
266
|
+
})}\n`,
|
|
267
|
+
);
|
|
268
|
+
process.exitCode = 1;
|
|
269
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { lstat, readdir, readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
const filesystem = {
|
|
6
|
+
astro: { roots: ['src/pages'], page: /\.(astro|md|mdx|html)$/ },
|
|
7
|
+
next: { roots: ['app', 'src/app'], page: /^page\.[cm]?[jt]sx?$/ },
|
|
8
|
+
sveltekit: { roots: ['src/routes'], page: /^\+page(?:@[\w-]*)?\.svelte$/ },
|
|
9
|
+
nuxt: { roots: ['app/pages', 'pages'], page: /\.vue$/ },
|
|
10
|
+
};
|
|
11
|
+
const tableRouters = new Set(['angular', 'react-router', 'react-vite', 'vue-vite']);
|
|
12
|
+
const safeName = /^[\p{L}\p{M}\p{N}_.~\-\[\]()+@=:*]+$/u;
|
|
13
|
+
|
|
14
|
+
// Return private review candidates only. Never parse module code, run loaders or read page bodies.
|
|
15
|
+
export async function discoverRoutes(framework, project, tableFile) {
|
|
16
|
+
if (!Object.hasOwn(filesystem, framework) && !tableRouters.has(framework))
|
|
17
|
+
throw Error('unknown-framework');
|
|
18
|
+
const root = await realpath(project);
|
|
19
|
+
const routes = [],
|
|
20
|
+
diagnostics = [];
|
|
21
|
+
let visited = 0;
|
|
22
|
+
function record(raw, source) {
|
|
23
|
+
if (
|
|
24
|
+
typeof raw !== 'string' ||
|
|
25
|
+
raw.split('/').some((segment) => segment === '..' || segment === '.') ||
|
|
26
|
+
raw.includes('\\') ||
|
|
27
|
+
!raw.startsWith('/')
|
|
28
|
+
)
|
|
29
|
+
throw Error('invalid-route-record');
|
|
30
|
+
if (++visited > 20000) throw Error('route-limit');
|
|
31
|
+
const route = raw.replace(/\/+$/, '') || '/';
|
|
32
|
+
const id =
|
|
33
|
+
'route-' +
|
|
34
|
+
createHash('sha256')
|
|
35
|
+
.update(JSON.stringify([framework, source, route]))
|
|
36
|
+
.digest('hex')
|
|
37
|
+
.slice(0, 24);
|
|
38
|
+
routes.push({
|
|
39
|
+
id,
|
|
40
|
+
route,
|
|
41
|
+
source,
|
|
42
|
+
visibility: 'unknown',
|
|
43
|
+
requiresParameters: /[\[\]:*$?]/.test(route),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function safeDir(rel) {
|
|
47
|
+
const dir = path.join(root, rel);
|
|
48
|
+
try {
|
|
49
|
+
if (!(await lstat(dir)).isDirectory() || (await realpath(dir)) !== dir) return false;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (error.code === 'ENOENT') return false;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
const rule = filesystem[framework];
|
|
57
|
+
if (rule) {
|
|
58
|
+
for (const prefix of rule.roots) {
|
|
59
|
+
if (!(await safeDir(prefix))) continue;
|
|
60
|
+
async function visit(folder) {
|
|
61
|
+
for (const entry of await readdir(path.join(root, prefix, folder), {
|
|
62
|
+
withFileTypes: true,
|
|
63
|
+
})) {
|
|
64
|
+
const name = entry.name;
|
|
65
|
+
if (
|
|
66
|
+
!safeName.test(name) ||
|
|
67
|
+
name.startsWith('.') ||
|
|
68
|
+
name.startsWith('_') ||
|
|
69
|
+
name.startsWith('@') ||
|
|
70
|
+
entry.isSymbolicLink()
|
|
71
|
+
)
|
|
72
|
+
continue;
|
|
73
|
+
const rel = path.posix.join(folder, name);
|
|
74
|
+
if (entry.isDirectory()) {
|
|
75
|
+
if (rel.split('/').length > 40) throw Error('route-depth-limit');
|
|
76
|
+
await visit(rel);
|
|
77
|
+
} else if (entry.isFile() && rule.page.test(name)) {
|
|
78
|
+
let parts = rel.split('/');
|
|
79
|
+
if (framework === 'next' || framework === 'sveltekit') parts.pop();
|
|
80
|
+
else {
|
|
81
|
+
parts[parts.length - 1] = name.replace(/\.[^.]+$/, '');
|
|
82
|
+
if (parts.at(-1) === 'index') parts.pop();
|
|
83
|
+
}
|
|
84
|
+
if (framework === 'next' || framework === 'sveltekit')
|
|
85
|
+
parts = parts.filter((part) => !/^\([^()]+\)$/.test(part));
|
|
86
|
+
record('/' + parts.join('/'), prefix + '/' + rel);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
await visit('');
|
|
91
|
+
if (framework === 'next' || framework === 'nuxt') break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
let table = tableFile;
|
|
95
|
+
if (!table && framework === 'angular' && (await safeDir('dist'))) {
|
|
96
|
+
try {
|
|
97
|
+
if ((await lstat(path.join(root, 'dist/prerendered-routes.json'))).isFile())
|
|
98
|
+
table = 'dist/prerendered-routes.json';
|
|
99
|
+
} catch (e) {
|
|
100
|
+
if (e.code !== 'ENOENT') throw e;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (table) {
|
|
104
|
+
const absolute = path.resolve(root, table),
|
|
105
|
+
relative = path.relative(root, absolute);
|
|
106
|
+
if (
|
|
107
|
+
relative.startsWith('..' + path.sep) ||
|
|
108
|
+
relative === '..' ||
|
|
109
|
+
path.isAbsolute(relative) ||
|
|
110
|
+
absolute !== (await realpath(absolute))
|
|
111
|
+
)
|
|
112
|
+
throw Error('unsafe-route-table');
|
|
113
|
+
const stat = await lstat(absolute);
|
|
114
|
+
if (!stat.isFile() || stat.size > 2 * 1024 * 1024) throw Error('invalid-route-table');
|
|
115
|
+
const value = JSON.parse(await readFile(absolute, 'utf8'));
|
|
116
|
+
const source = relative.split(path.sep).join('/');
|
|
117
|
+
if (Array.isArray(value)) {
|
|
118
|
+
const add = (row, parent, depth) => {
|
|
119
|
+
if (depth > 40) throw Error('route-depth-limit');
|
|
120
|
+
if (typeof row === 'string') {
|
|
121
|
+
record(row.startsWith('/') ? row : parent + '/' + row, source);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (
|
|
125
|
+
!row ||
|
|
126
|
+
typeof row !== 'object' ||
|
|
127
|
+
(row.path !== undefined && typeof row.path !== 'string')
|
|
128
|
+
)
|
|
129
|
+
throw Error('invalid-route-record');
|
|
130
|
+
const route = row.path?.startsWith('/')
|
|
131
|
+
? row.path
|
|
132
|
+
: [parent, row.path].filter(Boolean).join('/').replace(/^\/?/, '/');
|
|
133
|
+
if (row.path || row.index) record(route || '/', source);
|
|
134
|
+
if (row.children !== undefined) {
|
|
135
|
+
if (!Array.isArray(row.children)) throw Error('invalid-route-record');
|
|
136
|
+
for (const child of row.children) add(child, route === '/' ? '' : route, depth + 1);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
for (const row of value) add(row, '', 0);
|
|
140
|
+
} else if (
|
|
141
|
+
framework === 'angular' &&
|
|
142
|
+
value?.routes &&
|
|
143
|
+
typeof value.routes === 'object' &&
|
|
144
|
+
!Array.isArray(value.routes)
|
|
145
|
+
) {
|
|
146
|
+
for (const key of Object.keys(value.routes)) record(key, source);
|
|
147
|
+
} else throw Error('invalid-route-table');
|
|
148
|
+
}
|
|
149
|
+
if (!routes.length)
|
|
150
|
+
diagnostics.push(
|
|
151
|
+
tableRouters.has(framework) ? 'supply-exported-route-table' : 'no-conventional-pages',
|
|
152
|
+
);
|
|
153
|
+
const unique = new Map(routes.map((r) => [r.id, r]));
|
|
154
|
+
return {
|
|
155
|
+
status: 'discovered',
|
|
156
|
+
framework,
|
|
157
|
+
routes: [...unique.values()].sort((a, b) =>
|
|
158
|
+
a.route < b.route ? -1 : a.route > b.route ? 1 : a.id.localeCompare(b.id),
|
|
159
|
+
),
|
|
160
|
+
diagnostics,
|
|
161
|
+
};
|
|
162
|
+
}
|