@ggui-ai/cli 0.1.0-rc.1
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 +201 -0
- package/README.md +96 -0
- package/dist/agent-resolution.d.ts +41 -0
- package/dist/agent-resolution.d.ts.map +1 -0
- package/dist/agent-resolution.js +80 -0
- package/dist/api-client.d.ts +59 -0
- package/dist/api-client.d.ts.map +1 -0
- package/dist/api-client.js +145 -0
- package/dist/auth-keys.d.ts +10 -0
- package/dist/auth-keys.d.ts.map +1 -0
- package/dist/auth-keys.js +382 -0
- package/dist/auth-login.d.ts +11 -0
- package/dist/auth-login.d.ts.map +1 -0
- package/dist/auth-login.js +231 -0
- package/dist/auth-logout.d.ts +3 -0
- package/dist/auth-logout.d.ts.map +1 -0
- package/dist/auth-logout.js +32 -0
- package/dist/auth-store.d.ts +67 -0
- package/dist/auth-store.d.ts.map +1 -0
- package/dist/auth-store.js +150 -0
- package/dist/auth-whoami.d.ts +3 -0
- package/dist/auth-whoami.d.ts.map +1 -0
- package/dist/auth-whoami.js +52 -0
- package/dist/blueprint-command.d.ts +3 -0
- package/dist/blueprint-command.d.ts.map +1 -0
- package/dist/blueprint-command.js +203 -0
- package/dist/blueprint-create.d.ts +71 -0
- package/dist/blueprint-create.d.ts.map +1 -0
- package/dist/blueprint-create.js +377 -0
- package/dist/byok-resolver.d.ts +141 -0
- package/dist/byok-resolver.d.ts.map +1 -0
- package/dist/byok-resolver.js +89 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1066 -0
- package/dist/dev-command.d.ts +169 -0
- package/dist/dev-command.d.ts.map +1 -0
- package/dist/dev-command.js +222 -0
- package/dist/gadget-command.d.ts +3 -0
- package/dist/gadget-command.d.ts.map +1 -0
- package/dist/gadget-command.js +181 -0
- package/dist/gadget-create.d.ts +106 -0
- package/dist/gadget-create.d.ts.map +1 -0
- package/dist/gadget-create.js +492 -0
- package/dist/generation-probe.d.ts +135 -0
- package/dist/generation-probe.d.ts.map +1 -0
- package/dist/generation-probe.js +132 -0
- package/dist/internal/artifact-install.d.ts +150 -0
- package/dist/internal/artifact-install.d.ts.map +1 -0
- package/dist/internal/artifact-install.js +1019 -0
- package/dist/internal/artifact-publish.d.ts +204 -0
- package/dist/internal/artifact-publish.d.ts.map +1 -0
- package/dist/internal/artifact-publish.js +1141 -0
- package/dist/internal/artifact-search.d.ts +155 -0
- package/dist/internal/artifact-search.d.ts.map +1 -0
- package/dist/internal/artifact-search.js +539 -0
- package/dist/internal/artifact-uninstall.d.ts +30 -0
- package/dist/internal/artifact-uninstall.d.ts.map +1 -0
- package/dist/internal/artifact-uninstall.js +205 -0
- package/dist/internal/auth-cache.d.ts +40 -0
- package/dist/internal/auth-cache.d.ts.map +1 -0
- package/dist/internal/auth-cache.js +133 -0
- package/dist/internal/auth-strategy.d.ts +89 -0
- package/dist/internal/auth-strategy.d.ts.map +1 -0
- package/dist/internal/auth-strategy.js +132 -0
- package/dist/internal/ggui-json.d.ts +53 -0
- package/dist/internal/ggui-json.d.ts.map +1 -0
- package/dist/internal/ggui-json.js +92 -0
- package/dist/internal/key-store.d.ts +43 -0
- package/dist/internal/key-store.d.ts.map +1 -0
- package/dist/internal/key-store.js +128 -0
- package/dist/internal/oidc-token.d.ts +52 -0
- package/dist/internal/oidc-token.d.ts.map +1 -0
- package/dist/internal/oidc-token.js +351 -0
- package/dist/internal/register-author-key.d.ts +44 -0
- package/dist/internal/register-author-key.d.ts.map +1 -0
- package/dist/internal/register-author-key.js +212 -0
- package/dist/local-keys.d.ts +47 -0
- package/dist/local-keys.d.ts.map +1 -0
- package/dist/local-keys.js +115 -0
- package/dist/mcp-backend.d.ts +356 -0
- package/dist/mcp-backend.d.ts.map +1 -0
- package/dist/mcp-backend.js +668 -0
- package/dist/paths.d.ts +77 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +119 -0
- package/dist/persistent-secrets.d.ts +28 -0
- package/dist/persistent-secrets.d.ts.map +1 -0
- package/dist/persistent-secrets.js +94 -0
- package/dist/persistent-short-code-index.d.ts +14 -0
- package/dist/persistent-short-code-index.d.ts.map +1 -0
- package/dist/persistent-short-code-index.js +39 -0
- package/dist/persistent-stores.d.ts +12 -0
- package/dist/persistent-stores.d.ts.map +1 -0
- package/dist/persistent-stores.js +42 -0
- package/dist/serve-command.d.ts +500 -0
- package/dist/serve-command.d.ts.map +1 -0
- package/dist/serve-command.js +616 -0
- package/dist/serve-fallback.d.ts +64 -0
- package/dist/serve-fallback.d.ts.map +1 -0
- package/dist/serve-fallback.js +122 -0
- package/dist/spawn-cloudflared.d.ts +61 -0
- package/dist/spawn-cloudflared.d.ts.map +1 -0
- package/dist/spawn-cloudflared.js +167 -0
- package/dist/theme-command.d.ts +4 -0
- package/dist/theme-command.d.ts.map +1 -0
- package/dist/theme-command.js +154 -0
- package/dist/theme-file-uploader.d.ts +8 -0
- package/dist/theme-file-uploader.d.ts.map +1 -0
- package/dist/theme-file-uploader.js +58 -0
- package/dist/theme-writer.d.ts +8 -0
- package/dist/theme-writer.d.ts.map +1 -0
- package/dist/theme-writer.js +103 -0
- package/dist/tunnel-provider.d.ts +273 -0
- package/dist/tunnel-provider.d.ts.map +1 -0
- package/dist/tunnel-provider.js +142 -0
- package/package.json +81 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `themeWriter` factory for `ggui serve`.
|
|
3
|
+
*
|
|
4
|
+
* The OSS server (`@ggui-ai/mcp-server`) defines a `ThemeWriter` seam
|
|
5
|
+
* but never touches the filesystem itself — it only knows the shape it
|
|
6
|
+
* wants to persist (`ThemeConfig | null`). The CLI plugs in the side
|
|
7
|
+
* that knows where `ggui.json` lives.
|
|
8
|
+
*
|
|
9
|
+
* Behavior:
|
|
10
|
+
*
|
|
11
|
+
* - Read the existing `ggui.json`.
|
|
12
|
+
* - Parse as JSON. Refuse on malformed input — we will not paper over
|
|
13
|
+
* a manifest the CLI can't round-trip safely.
|
|
14
|
+
* - Mutate exactly the `theme` field (`null` argument removes it
|
|
15
|
+
* entirely so the manifest falls back to the default-theme branch).
|
|
16
|
+
* - Write back atomically (`tmp` + `rename`) with 2-space indent,
|
|
17
|
+
* trailing newline. All other fields are preserved verbatim — this
|
|
18
|
+
* is a JSON round-trip, NOT a re-serialize-from-schema, so unknown
|
|
19
|
+
* fields the operator added (forward-compat schema, custom
|
|
20
|
+
* extensions) survive a Save.
|
|
21
|
+
*
|
|
22
|
+
* What v1 does NOT preserve:
|
|
23
|
+
*
|
|
24
|
+
* - JSON5-style comments (the schema doesn't allow them but operators
|
|
25
|
+
* occasionally have a fork that does).
|
|
26
|
+
* - Original indentation (always rewritten to 2-space).
|
|
27
|
+
* - Trailing-comma tolerance (JSON.parse already rejects, so the
|
|
28
|
+
* refusal is upstream of us).
|
|
29
|
+
*
|
|
30
|
+
* Operators who need comment-preserving edits should hand-edit the
|
|
31
|
+
* file; the picker is a write-through path for the common case.
|
|
32
|
+
*
|
|
33
|
+
* Validation: `ThemeConfig` was already validated upstream by
|
|
34
|
+
* `mountDevtoolThemeRoutes` (Zod safeParse against `ThemeConfigSchema`)
|
|
35
|
+
* before reaching the writer. We re-do the type-narrow shape check on
|
|
36
|
+
* the resulting object purely so a programmatic embedder that called
|
|
37
|
+
* `themeWriter` directly with a malformed value gets a 400-equivalent
|
|
38
|
+
* `Error` instead of writing nonsense to disk.
|
|
39
|
+
*/
|
|
40
|
+
import { dirname } from 'node:path';
|
|
41
|
+
import { promises as fs } from 'node:fs';
|
|
42
|
+
import { ThemeConfigSchema, } from '@ggui-ai/project-config';
|
|
43
|
+
/**
|
|
44
|
+
* Build a {@link ThemeWriter} bound to the manifest at `manifestPath`.
|
|
45
|
+
* The returned function is async, idempotent for equal inputs, and
|
|
46
|
+
* crash-safe via a `tmp` + `rename` write.
|
|
47
|
+
*/
|
|
48
|
+
export function createThemeWriter(manifestPath) {
|
|
49
|
+
return async (config) => {
|
|
50
|
+
if (config !== null) {
|
|
51
|
+
// Belt-and-braces re-validate. The server's transport already
|
|
52
|
+
// does this, but the seam is public — any consumer that reaches
|
|
53
|
+
// it directly should still get a clear refusal rather than a
|
|
54
|
+
// silent shape leak into ggui.json.
|
|
55
|
+
const result = ThemeConfigSchema.safeParse(config);
|
|
56
|
+
if (!result.success) {
|
|
57
|
+
throw new Error(`themeWriter: invalid ThemeConfig — ${JSON.stringify(result.error.flatten())}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = await fs.readFile(manifestPath, 'utf-8');
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
// ENOENT = the manifest disappeared between server boot and
|
|
66
|
+
// first save. Surface clearly so the operator sees the cause
|
|
67
|
+
// instead of a silent data-loss-by-creation.
|
|
68
|
+
throw new Error(`themeWriter: failed to read ${manifestPath} — ${err instanceof Error ? err.message : String(err)}`);
|
|
69
|
+
}
|
|
70
|
+
let parsed;
|
|
71
|
+
try {
|
|
72
|
+
const json = JSON.parse(raw);
|
|
73
|
+
if (typeof json !== 'object' || json === null || Array.isArray(json)) {
|
|
74
|
+
throw new Error('manifest is not a JSON object');
|
|
75
|
+
}
|
|
76
|
+
parsed = json;
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
throw new Error(`themeWriter: ${manifestPath} is not parseable JSON — ${err instanceof Error ? err.message : String(err)}`);
|
|
80
|
+
}
|
|
81
|
+
if (config === null) {
|
|
82
|
+
delete parsed['theme'];
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
parsed['theme'] = config;
|
|
86
|
+
}
|
|
87
|
+
const next = `${JSON.stringify(parsed, null, 2)}\n`;
|
|
88
|
+
// Atomic write via tmp + rename — same pattern as
|
|
89
|
+
// `oauth-providers-store::writeFileShape`. A crash mid-write leaves
|
|
90
|
+
// the original `ggui.json` intact.
|
|
91
|
+
const tmp = `${manifestPath}.tmp`;
|
|
92
|
+
await fs.mkdir(dirname(manifestPath), { recursive: true });
|
|
93
|
+
const handle = await fs.open(tmp, 'w');
|
|
94
|
+
try {
|
|
95
|
+
await handle.writeFile(next, 'utf-8');
|
|
96
|
+
await handle.sync();
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
await handle.close();
|
|
100
|
+
}
|
|
101
|
+
await fs.rename(tmp, manifestPath);
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TunnelProvider` — narrow seam for managed-mode tunnel bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* Design:
|
|
5
|
+
*
|
|
6
|
+
* - `@ggui-ai/dev-stack` stays local-engine only. No tunnel code
|
|
7
|
+
* lives inside the dev-stack barrel. The CLI is the layer that
|
|
8
|
+
* decides whether to wrap a running dev-stack host in a managed
|
|
9
|
+
* tunnel.
|
|
10
|
+
* - `TunnelProvider` is intentionally the minimum interface a
|
|
11
|
+
* provider needs to satisfy. Future providers (Cloudflare
|
|
12
|
+
* tunnel, Guuey-managed auth-proxy tunnel, Tailscale funnel,
|
|
13
|
+
* ngrok-style forwarder) plug in behind this seam without
|
|
14
|
+
* touching dev-stack or the core `runDev` coordination logic.
|
|
15
|
+
* - The current scaffold ships a **null** provider that always
|
|
16
|
+
* returns `{ status: 'unavailable' }`. This proves the
|
|
17
|
+
* architecture end-to-end without shipping a public-tunnel
|
|
18
|
+
* story before remote authentication is resolved.
|
|
19
|
+
*
|
|
20
|
+
* Non-negotiable invariants:
|
|
21
|
+
*
|
|
22
|
+
* 1. `open()` MUST NOT throw for missing configuration. It returns
|
|
23
|
+
* `{ status: 'unavailable' }` so the CLI can print a clean
|
|
24
|
+
* status line and keep local dev running unchanged. Only
|
|
25
|
+
* genuinely exceptional failures (invariant violations in the
|
|
26
|
+
* provider itself) are allowed to throw.
|
|
27
|
+
* 2. Providers do not own the decision to expose `/hub*` over the
|
|
28
|
+
* tunnel. `TunnelContext.authToken` is surfaced so providers
|
|
29
|
+
* CAN use it for bearer-authenticated upstream calls — but
|
|
30
|
+
* providers MUST NOT leak it into public tunnel responses.
|
|
31
|
+
* Remote hub exposure requires a separate auth layer
|
|
32
|
+
* (see plan doc §"Open: remote auth").
|
|
33
|
+
* 3. `close()` on a ready session MUST be idempotent + safe to
|
|
34
|
+
* call after the underlying `AbortSignal` has fired.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Snapshot of the running local dev stack handed to a tunnel
|
|
38
|
+
* provider. Readonly — the provider inspects and wraps, but cannot
|
|
39
|
+
* reconfigure the local host.
|
|
40
|
+
*/
|
|
41
|
+
export interface TunnelContext {
|
|
42
|
+
/**
|
|
43
|
+
* Fully-qualified URL of the local dev-stack HTTP host, e.g.
|
|
44
|
+
* `http://127.0.0.1:6780`. Never a wildcard bind or a path —
|
|
45
|
+
* the CLI normalises before handing to the provider.
|
|
46
|
+
*/
|
|
47
|
+
readonly localUrl: string;
|
|
48
|
+
/**
|
|
49
|
+
* Bearer token the dev-stack accepts for gated endpoints
|
|
50
|
+
* (`/uis/*`, `/events`, `/runtime/*`). `null` when the server
|
|
51
|
+
* was started without a security policy (tests).
|
|
52
|
+
*
|
|
53
|
+
* Providers MAY use this to make bearer-authenticated upstream
|
|
54
|
+
* calls against the local host (e.g. to probe `/health` or
|
|
55
|
+
* pre-fetch state during bootstrap). Providers MUST NOT pass
|
|
56
|
+
* the bearer through to public tunnel clients — the bearer is
|
|
57
|
+
* a per-run secret, not a user credential.
|
|
58
|
+
*/
|
|
59
|
+
readonly authToken: string | null;
|
|
60
|
+
/**
|
|
61
|
+
* Project identity surfaced from `ggui.json`. Providers use
|
|
62
|
+
* this as a stable handle when requesting a remote URL
|
|
63
|
+
* ("open a tunnel for `weather-bot`"). Kept narrow on purpose;
|
|
64
|
+
* richer metadata can be added later without a breaking change.
|
|
65
|
+
*/
|
|
66
|
+
readonly project: {
|
|
67
|
+
readonly slug: string;
|
|
68
|
+
readonly name: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Port the supervised agent runtime is bound on, when an `--agent`
|
|
72
|
+
* was supplied AND the CLI orchestration asked the dev-stack to
|
|
73
|
+
* wire a specific port (`DevOptions.runtimePort`). `null` when:
|
|
74
|
+
*
|
|
75
|
+
* - no agent runtime was supplied (`--agent` absent), OR
|
|
76
|
+
* - the agent runtime was supplied but the CLI did not ask for a
|
|
77
|
+
* specific port (e.g. the user ran `ggui dev --agent …` without
|
|
78
|
+
* `--tunnel`).
|
|
79
|
+
*
|
|
80
|
+
* Bridge-style tunnel providers use this to construct a
|
|
81
|
+
* `UrlProxy('http://127.0.0.1:<runtimePort>')` or equivalent for
|
|
82
|
+
* forwarding inbound platform traffic to the local agent. Other
|
|
83
|
+
* providers (forwarders that wrap `localUrl` end-to-end) MAY
|
|
84
|
+
* ignore it.
|
|
85
|
+
*/
|
|
86
|
+
readonly runtimePort: number | null;
|
|
87
|
+
/**
|
|
88
|
+
* Lifetime signal. Aborted when the CLI is shutting down the
|
|
89
|
+
* dev stack (Ctrl-C, SIGTERM, server.close()). Providers that
|
|
90
|
+
* maintain long-lived connections MUST wire this signal into
|
|
91
|
+
* their underlying transport so the local server shutdown is
|
|
92
|
+
* not blocked by a dangling tunnel socket.
|
|
93
|
+
*/
|
|
94
|
+
readonly signal: AbortSignal;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Discriminated result of `TunnelProvider.open(ctx)`.
|
|
98
|
+
*
|
|
99
|
+
* - `ready` → managed tunnel is up; remoteUrl is printable.
|
|
100
|
+
* - `unavailable` → provider correctly declined (missing
|
|
101
|
+
* configuration, expired auth, offline), and
|
|
102
|
+
* the CLI should fall back to local-only.
|
|
103
|
+
* `reason` is user-facing; `hint` is an
|
|
104
|
+
* optional actionable suggestion.
|
|
105
|
+
*/
|
|
106
|
+
export type TunnelSession = TunnelSessionReady | TunnelSessionUnavailable;
|
|
107
|
+
export interface TunnelSessionReady {
|
|
108
|
+
readonly status: 'ready';
|
|
109
|
+
/** Public-facing URL the developer can share. */
|
|
110
|
+
readonly remoteUrl: string;
|
|
111
|
+
/**
|
|
112
|
+
* Shut down the tunnel session. Idempotent — calling twice (or
|
|
113
|
+
* calling after `signal` fired) is a no-op. Resolves when the
|
|
114
|
+
* underlying transport has released its resources.
|
|
115
|
+
*/
|
|
116
|
+
close(): Promise<void>;
|
|
117
|
+
}
|
|
118
|
+
export interface TunnelSessionUnavailable {
|
|
119
|
+
readonly status: 'unavailable';
|
|
120
|
+
/**
|
|
121
|
+
* One-line user-facing reason. Printed in the CLI banner, so
|
|
122
|
+
* keep it short and concrete (e.g. `"not configured"`,
|
|
123
|
+
* `"login expired"`, `"network unreachable"`).
|
|
124
|
+
*/
|
|
125
|
+
readonly reason: string;
|
|
126
|
+
/**
|
|
127
|
+
* Optional follow-up action — typically a command the user can
|
|
128
|
+
* run to unblock (`"run \`<your-cli> login\`"`). May be omitted
|
|
129
|
+
* when the reason is self-evident.
|
|
130
|
+
*/
|
|
131
|
+
readonly hint?: string;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* A single managed-mode tunnel implementation. Providers are
|
|
135
|
+
* pluggable by construction — a CLI host picks one at boot time
|
|
136
|
+
* (flag, env var, discovery) and hands it to the orchestrator.
|
|
137
|
+
*/
|
|
138
|
+
export interface TunnelProvider {
|
|
139
|
+
/**
|
|
140
|
+
* Human-readable provider name for banner / logs. Keep short
|
|
141
|
+
* (`"null"`, `"cloudflare"`). Not used for any protocol dispatch
|
|
142
|
+
* — display only.
|
|
143
|
+
*/
|
|
144
|
+
readonly name: string;
|
|
145
|
+
/**
|
|
146
|
+
* Attempt to establish a managed tunnel for the running local
|
|
147
|
+
* dev host. Non-throwing by contract — missing configuration is
|
|
148
|
+
* `{ status: 'unavailable', reason, hint }`, not an exception.
|
|
149
|
+
*
|
|
150
|
+
* Providers respecting `ctx.signal` MAY return before the
|
|
151
|
+
* tunnel is fully dialled if the signal fires during bootstrap.
|
|
152
|
+
*/
|
|
153
|
+
open(ctx: TunnelContext): Promise<TunnelSession>;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Reference "null" tunnel provider.
|
|
157
|
+
*
|
|
158
|
+
* Purpose:
|
|
159
|
+
*
|
|
160
|
+
* - Proves the `TunnelProvider` seam works end-to-end (the CLI
|
|
161
|
+
* can construct one, wire it into `runDev` orchestration, and
|
|
162
|
+
* print the expected banner line).
|
|
163
|
+
* - Never makes a network call, never reads config files, never
|
|
164
|
+
* spawns a subprocess. Safe to run under any environment
|
|
165
|
+
* policy (CI, sandboxes, sandboxed CI, locked-down laptops).
|
|
166
|
+
* - Acts as the default when `ggui dev --tunnel` runs without a
|
|
167
|
+
* configured provider. The user sees an honest "no provider
|
|
168
|
+
* configured" line instead of a cryptic network error.
|
|
169
|
+
*
|
|
170
|
+
* An optional `reason` override lets tests pin a specific
|
|
171
|
+
* status string without monkey-patching.
|
|
172
|
+
*/
|
|
173
|
+
export interface NullTunnelProviderOptions {
|
|
174
|
+
/**
|
|
175
|
+
* Override the default reason string. Useful for tests asserting
|
|
176
|
+
* the banner reads the expected copy, or for hosts that want to
|
|
177
|
+
* customise the "not configured" message without replacing the
|
|
178
|
+
* provider.
|
|
179
|
+
*/
|
|
180
|
+
readonly reason?: string;
|
|
181
|
+
/** Optional hint shown alongside the reason. */
|
|
182
|
+
readonly hint?: string;
|
|
183
|
+
}
|
|
184
|
+
export declare function createNullTunnelProvider(options?: NullTunnelProviderOptions): TunnelProvider;
|
|
185
|
+
/**
|
|
186
|
+
* Contract a tunnel-provider module must satisfy for dynamic
|
|
187
|
+
* discovery. Modules export either a default factory or a named
|
|
188
|
+
* `createTunnelProvider` factory — both shapes are tried in that
|
|
189
|
+
* order. The factory takes no arguments for the first slice; future
|
|
190
|
+
* revisions MAY pass options (project identity, login snapshot)
|
|
191
|
+
* behind a backwards-compatible shim.
|
|
192
|
+
*
|
|
193
|
+
* Kept as a module shape rather than a typed `require` because the
|
|
194
|
+
* target module path is controlled by the user's env, not by the
|
|
195
|
+
* open CLI's dependency graph. The discovery wrapper below validates
|
|
196
|
+
* the shape at runtime and falls back cleanly on any mismatch.
|
|
197
|
+
*/
|
|
198
|
+
export interface TunnelProviderModule {
|
|
199
|
+
/** Preferred shape. */
|
|
200
|
+
readonly createTunnelProvider?: () => TunnelProvider | Promise<TunnelProvider>;
|
|
201
|
+
/** Backwards-compatible shape — default export returning the provider. */
|
|
202
|
+
readonly default?: TunnelProvider | (() => TunnelProvider | Promise<TunnelProvider>);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Options for the provider-discovery helper. `resolve` is a test
|
|
206
|
+
* seam that stands in for `import()` — production callers pass no
|
|
207
|
+
* options and the helper uses the real dynamic import.
|
|
208
|
+
*/
|
|
209
|
+
export interface DiscoverTunnelProviderOptions {
|
|
210
|
+
/**
|
|
211
|
+
* Module specifier to attempt. Default: `process.env.GGUI_TUNNEL_PROVIDER`.
|
|
212
|
+
* When unset / empty, no discovery is attempted and the caller
|
|
213
|
+
* falls back to the null provider with reason "no provider
|
|
214
|
+
* configured".
|
|
215
|
+
*/
|
|
216
|
+
readonly moduleSpecifier?: string | null;
|
|
217
|
+
/**
|
|
218
|
+
* Test seam — receives the module specifier and returns the
|
|
219
|
+
* imported module (or throws to simulate a failed import).
|
|
220
|
+
* Production callers leave this undefined; the helper then uses
|
|
221
|
+
* the real dynamic `import()`.
|
|
222
|
+
*/
|
|
223
|
+
readonly resolve?: (specifier: string) => Promise<unknown>;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Discriminated result of `discoverTunnelProvider`.
|
|
227
|
+
*
|
|
228
|
+
* - `found` → the module path resolved AND its factory produced a
|
|
229
|
+
* provider. The CLI uses `provider` directly.
|
|
230
|
+
* - `none` → no module specifier set; expected for OSS users who
|
|
231
|
+
* haven't installed a private provider. Caller falls
|
|
232
|
+
* back to the null provider with a specific reason.
|
|
233
|
+
* - `error` → a specifier was configured but loading / instantiating
|
|
234
|
+
* failed. Caller falls back to the null provider with
|
|
235
|
+
* the failure reason surfaced so the user can act
|
|
236
|
+
* (fix path, reinstall, etc.).
|
|
237
|
+
*/
|
|
238
|
+
export type TunnelProviderDiscovery = {
|
|
239
|
+
readonly kind: 'found';
|
|
240
|
+
readonly provider: TunnelProvider;
|
|
241
|
+
readonly moduleSpecifier: string;
|
|
242
|
+
} | {
|
|
243
|
+
readonly kind: 'none';
|
|
244
|
+
} | {
|
|
245
|
+
readonly kind: 'error';
|
|
246
|
+
readonly moduleSpecifier: string;
|
|
247
|
+
readonly reason: string;
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* Attempt to load a `TunnelProvider` from an external module whose
|
|
251
|
+
* path is given by `process.env.GGUI_TUNNEL_PROVIDER` (or the
|
|
252
|
+
* `moduleSpecifier` override).
|
|
253
|
+
*
|
|
254
|
+
* Discovery mechanics:
|
|
255
|
+
*
|
|
256
|
+
* 1. If no specifier is configured → `{ kind: 'none' }`. The
|
|
257
|
+
* caller prints "no tunnel provider configured" and uses the
|
|
258
|
+
* null provider.
|
|
259
|
+
* 2. Dynamic-import the specifier. On any thrown error →
|
|
260
|
+
* `{ kind: 'error', reason }` — caller prints the reason.
|
|
261
|
+
* 3. Read `module.createTunnelProvider` first, then `module.default`.
|
|
262
|
+
* Both may be factory functions or (for `default` only) an
|
|
263
|
+
* already-built provider object. Anything else → `error`.
|
|
264
|
+
* 4. Invoke the factory (if callable). If it throws → `error`.
|
|
265
|
+
* 5. Validate the returned value has `.name: string` + `.open`
|
|
266
|
+
* callable. If not → `error`.
|
|
267
|
+
* 6. Success → `{ kind: 'found', provider, moduleSpecifier }`.
|
|
268
|
+
*
|
|
269
|
+
* Never throws. The null-provider fallback is the single recovery
|
|
270
|
+
* path across every failure mode.
|
|
271
|
+
*/
|
|
272
|
+
export declare function discoverTunnelProvider(options?: DiscoverTunnelProviderOptions): Promise<TunnelProviderDiscovery>;
|
|
273
|
+
//# sourceMappingURL=tunnel-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel-provider.d.ts","sourceRoot":"","sources":["../src/tunnel-provider.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `TunnelProvider` — narrow seam for managed-mode tunnel bootstrap.
|
|
3
|
+
*
|
|
4
|
+
* Design:
|
|
5
|
+
*
|
|
6
|
+
* - `@ggui-ai/dev-stack` stays local-engine only. No tunnel code
|
|
7
|
+
* lives inside the dev-stack barrel. The CLI is the layer that
|
|
8
|
+
* decides whether to wrap a running dev-stack host in a managed
|
|
9
|
+
* tunnel.
|
|
10
|
+
* - `TunnelProvider` is intentionally the minimum interface a
|
|
11
|
+
* provider needs to satisfy. Future providers (Cloudflare
|
|
12
|
+
* tunnel, Guuey-managed auth-proxy tunnel, Tailscale funnel,
|
|
13
|
+
* ngrok-style forwarder) plug in behind this seam without
|
|
14
|
+
* touching dev-stack or the core `runDev` coordination logic.
|
|
15
|
+
* - The current scaffold ships a **null** provider that always
|
|
16
|
+
* returns `{ status: 'unavailable' }`. This proves the
|
|
17
|
+
* architecture end-to-end without shipping a public-tunnel
|
|
18
|
+
* story before remote authentication is resolved.
|
|
19
|
+
*
|
|
20
|
+
* Non-negotiable invariants:
|
|
21
|
+
*
|
|
22
|
+
* 1. `open()` MUST NOT throw for missing configuration. It returns
|
|
23
|
+
* `{ status: 'unavailable' }` so the CLI can print a clean
|
|
24
|
+
* status line and keep local dev running unchanged. Only
|
|
25
|
+
* genuinely exceptional failures (invariant violations in the
|
|
26
|
+
* provider itself) are allowed to throw.
|
|
27
|
+
* 2. Providers do not own the decision to expose `/hub*` over the
|
|
28
|
+
* tunnel. `TunnelContext.authToken` is surfaced so providers
|
|
29
|
+
* CAN use it for bearer-authenticated upstream calls — but
|
|
30
|
+
* providers MUST NOT leak it into public tunnel responses.
|
|
31
|
+
* Remote hub exposure requires a separate auth layer
|
|
32
|
+
* (see plan doc §"Open: remote auth").
|
|
33
|
+
* 3. `close()` on a ready session MUST be idempotent + safe to
|
|
34
|
+
* call after the underlying `AbortSignal` has fired.
|
|
35
|
+
*/
|
|
36
|
+
export function createNullTunnelProvider(options = {}) {
|
|
37
|
+
const reason = options.reason ?? 'no tunnel provider configured';
|
|
38
|
+
const hint = options.hint;
|
|
39
|
+
return {
|
|
40
|
+
name: 'null',
|
|
41
|
+
async open(_ctx) {
|
|
42
|
+
// Keep the context parameter so the interface is exercised;
|
|
43
|
+
// the null provider ignores every field by design.
|
|
44
|
+
void _ctx;
|
|
45
|
+
const result = hint
|
|
46
|
+
? { status: 'unavailable', reason, hint }
|
|
47
|
+
: { status: 'unavailable', reason };
|
|
48
|
+
return result;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Attempt to load a `TunnelProvider` from an external module whose
|
|
54
|
+
* path is given by `process.env.GGUI_TUNNEL_PROVIDER` (or the
|
|
55
|
+
* `moduleSpecifier` override).
|
|
56
|
+
*
|
|
57
|
+
* Discovery mechanics:
|
|
58
|
+
*
|
|
59
|
+
* 1. If no specifier is configured → `{ kind: 'none' }`. The
|
|
60
|
+
* caller prints "no tunnel provider configured" and uses the
|
|
61
|
+
* null provider.
|
|
62
|
+
* 2. Dynamic-import the specifier. On any thrown error →
|
|
63
|
+
* `{ kind: 'error', reason }` — caller prints the reason.
|
|
64
|
+
* 3. Read `module.createTunnelProvider` first, then `module.default`.
|
|
65
|
+
* Both may be factory functions or (for `default` only) an
|
|
66
|
+
* already-built provider object. Anything else → `error`.
|
|
67
|
+
* 4. Invoke the factory (if callable). If it throws → `error`.
|
|
68
|
+
* 5. Validate the returned value has `.name: string` + `.open`
|
|
69
|
+
* callable. If not → `error`.
|
|
70
|
+
* 6. Success → `{ kind: 'found', provider, moduleSpecifier }`.
|
|
71
|
+
*
|
|
72
|
+
* Never throws. The null-provider fallback is the single recovery
|
|
73
|
+
* path across every failure mode.
|
|
74
|
+
*/
|
|
75
|
+
export async function discoverTunnelProvider(options = {}) {
|
|
76
|
+
const specifier = options.moduleSpecifier !== undefined
|
|
77
|
+
? options.moduleSpecifier
|
|
78
|
+
: process.env.GGUI_TUNNEL_PROVIDER ?? null;
|
|
79
|
+
if (!specifier || specifier.length === 0) {
|
|
80
|
+
return { kind: 'none' };
|
|
81
|
+
}
|
|
82
|
+
const resolve = options.resolve ?? ((s) => import(s));
|
|
83
|
+
let mod;
|
|
84
|
+
try {
|
|
85
|
+
mod = await resolve(specifier);
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return {
|
|
89
|
+
kind: 'error',
|
|
90
|
+
moduleSpecifier: specifier,
|
|
91
|
+
reason: `failed to import "${specifier}": ${err instanceof Error ? err.message : String(err)}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (mod === null || typeof mod !== 'object') {
|
|
95
|
+
return {
|
|
96
|
+
kind: 'error',
|
|
97
|
+
moduleSpecifier: specifier,
|
|
98
|
+
reason: `module "${specifier}" did not export an object`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const typed = mod;
|
|
102
|
+
const factory = typed.createTunnelProvider ??
|
|
103
|
+
(typeof typed.default === 'function' ? typed.default : null);
|
|
104
|
+
const preBuilt = typed.createTunnelProvider === undefined && typeof typed.default === 'object' && typed.default !== null
|
|
105
|
+
? typed.default
|
|
106
|
+
: null;
|
|
107
|
+
let provider;
|
|
108
|
+
try {
|
|
109
|
+
if (preBuilt) {
|
|
110
|
+
provider = preBuilt;
|
|
111
|
+
}
|
|
112
|
+
else if (factory) {
|
|
113
|
+
const maybeProvider = await factory();
|
|
114
|
+
provider = maybeProvider;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
return {
|
|
118
|
+
kind: 'error',
|
|
119
|
+
moduleSpecifier: specifier,
|
|
120
|
+
reason: `module "${specifier}" exports neither \`createTunnelProvider\` nor a default export`,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
return {
|
|
126
|
+
kind: 'error',
|
|
127
|
+
moduleSpecifier: specifier,
|
|
128
|
+
reason: `provider factory in "${specifier}" threw: ${err instanceof Error ? err.message : String(err)}`,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (!provider ||
|
|
132
|
+
typeof provider !== 'object' ||
|
|
133
|
+
typeof provider.name !== 'string' ||
|
|
134
|
+
typeof provider.open !== 'function') {
|
|
135
|
+
return {
|
|
136
|
+
kind: 'error',
|
|
137
|
+
moduleSpecifier: specifier,
|
|
138
|
+
reason: `module "${specifier}" returned a value that is not a TunnelProvider`,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return { kind: 'found', provider, moduleSpecifier: specifier };
|
|
142
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ggui-ai/cli",
|
|
3
|
+
"version": "0.1.0-rc.1",
|
|
4
|
+
"description": "The `ggui` CLI — run the ggui protocol locally: dev server, self-hosted MCP server, and gadget / blueprint authoring. End-to-end OSS and self-hosted workflows; no account required.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ggui",
|
|
7
|
+
"cli",
|
|
8
|
+
"mcp",
|
|
9
|
+
"model-context-protocol",
|
|
10
|
+
"ai-agents",
|
|
11
|
+
"generative-ui",
|
|
12
|
+
"dev-server"
|
|
13
|
+
],
|
|
14
|
+
"license": "Apache-2.0",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"bin": {
|
|
17
|
+
"ggui": "./dist/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
"./tunnel-provider": {
|
|
21
|
+
"types": "./dist/tunnel-provider.d.ts",
|
|
22
|
+
"import": "./dist/tunnel-provider.js",
|
|
23
|
+
"default": "./dist/tunnel-provider.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@aws-sdk/client-cognito-identity-provider": "^3.600.0",
|
|
32
|
+
"@huggingface/transformers": "^3.0.0",
|
|
33
|
+
"@noble/hashes": "^1.6.0",
|
|
34
|
+
"esbuild": "^0.25.12",
|
|
35
|
+
"@ggui-ai/agent-runtime": "0.1.0-rc.1",
|
|
36
|
+
"@ggui-ai/artifact-manifest": "0.1.0-rc.1",
|
|
37
|
+
"@ggui-ai/design": "0.1.0-rc.1",
|
|
38
|
+
"@ggui-ai/dev-stack": "0.1.0-rc.1",
|
|
39
|
+
"@ggui-ai/gadget-signing": "0.1.0-rc.1",
|
|
40
|
+
"@ggui-ai/mcp-server": "0.1.0-rc.1",
|
|
41
|
+
"@ggui-ai/embedding-local": "0.1.0-rc.1",
|
|
42
|
+
"@ggui-ai/mcp-server-core": "0.1.0-rc.1",
|
|
43
|
+
"@ggui-ai/preview-a2ui": "0.1.0-rc.1",
|
|
44
|
+
"@ggui-ai/project-config": "0.1.0-rc.1",
|
|
45
|
+
"@ggui-ai/protocol": "0.1.0-rc.1",
|
|
46
|
+
"@ggui-ai/registry-core": "0.1.0-rc.1",
|
|
47
|
+
"@ggui-ai/iframe-runtime": "0.1.0-rc.1",
|
|
48
|
+
"@ggui-ai/mcp-server-handlers": "0.1.0-rc.1",
|
|
49
|
+
"@ggui-ai/ui-registry": "0.1.0-rc.1",
|
|
50
|
+
"@ggui-ai/ui-gen": "0.1.0-rc.1"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^24.0.0",
|
|
54
|
+
"typescript": "^5.0.0",
|
|
55
|
+
"vitest": "^3.0.0",
|
|
56
|
+
"zod": "^4.3.6"
|
|
57
|
+
},
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "https://github.com/ggui-ai/ggui.git",
|
|
61
|
+
"directory": "packages/ggui-cli"
|
|
62
|
+
},
|
|
63
|
+
"homepage": "https://github.com/ggui-ai/ggui/tree/main/packages/ggui-cli",
|
|
64
|
+
"bugs": {
|
|
65
|
+
"url": "https://github.com/ggui-ai/ggui/issues"
|
|
66
|
+
},
|
|
67
|
+
"publishConfig": {
|
|
68
|
+
"access": "public"
|
|
69
|
+
},
|
|
70
|
+
"engines": {
|
|
71
|
+
"node": ">=20.0.0"
|
|
72
|
+
},
|
|
73
|
+
"author": "ggui contributors <hello@ggui.ai>",
|
|
74
|
+
"scripts": {
|
|
75
|
+
"build": "tsc -p tsconfig.build.json && chmod +x dist/cli.js",
|
|
76
|
+
"dev": "tsc --watch",
|
|
77
|
+
"typecheck": "tsc --noEmit",
|
|
78
|
+
"test": "vitest run --passWithNoTests",
|
|
79
|
+
"test:watch": "vitest"
|
|
80
|
+
}
|
|
81
|
+
}
|