@cosmoops/connect-sdk 0.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 +34 -0
- package/README.md +446 -0
- package/dist/cjs/cli/bin.js +16 -0
- package/dist/cjs/cli/init.js +179 -0
- package/dist/cjs/client/components/account-sessions.js +112 -0
- package/dist/cjs/client/components/apps-grid.js +46 -0
- package/dist/cjs/client/components/auth-buttons.js +42 -0
- package/dist/cjs/client/components/feature-gate.js +61 -0
- package/dist/cjs/client/components/link-button.js +24 -0
- package/dist/cjs/client/components/logo.js +27 -0
- package/dist/cjs/client/components/plan-pricing.js +166 -0
- package/dist/cjs/client/components/plans-page.js +39 -0
- package/dist/cjs/client/components/plans.js +212 -0
- package/dist/cjs/client/components/readiness.js +117 -0
- package/dist/cjs/client/components/subscribe-dialog.js +60 -0
- package/dist/cjs/client/components/subscription.js +189 -0
- package/dist/cjs/client/components/usage-meter.js +68 -0
- package/dist/cjs/client/components/user-menu.js +172 -0
- package/dist/cjs/client/index.js +99 -0
- package/dist/cjs/client/mark.js +16 -0
- package/dist/cjs/client/provider.js +259 -0
- package/dist/cjs/client/use-checkout.js +173 -0
- package/dist/cjs/errors.js +74 -0
- package/dist/cjs/format-relative-time.js +18 -0
- package/dist/cjs/helpers.js +30 -0
- package/dist/cjs/index.js +22 -0
- package/dist/cjs/next/index.js +311 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/server/client.js +114 -0
- package/dist/cjs/server/handler.js +333 -0
- package/dist/cjs/server/http.js +160 -0
- package/dist/cjs/server/index.js +18 -0
- package/dist/cjs/server/resources/access.js +28 -0
- package/dist/cjs/server/resources/ai.js +36 -0
- package/dist/cjs/server/resources/apps.js +30 -0
- package/dist/cjs/server/resources/billing.js +71 -0
- package/dist/cjs/server/resources/drive.js +39 -0
- package/dist/cjs/server/resources/entitlements.js +49 -0
- package/dist/cjs/server/resources/identity.js +194 -0
- package/dist/cjs/server/resources/logs.js +41 -0
- package/dist/cjs/server/resources/otp.js +29 -0
- package/dist/cjs/server/resources/sessions.js +82 -0
- package/dist/cjs/types.js +15 -0
- package/dist/esm/cli/bin.js +13 -0
- package/dist/esm/cli/init.js +174 -0
- package/dist/esm/client/components/account-sessions.js +109 -0
- package/dist/esm/client/components/apps-grid.js +43 -0
- package/dist/esm/client/components/auth-buttons.js +38 -0
- package/dist/esm/client/components/feature-gate.js +57 -0
- package/dist/esm/client/components/link-button.js +21 -0
- package/dist/esm/client/components/logo.js +24 -0
- package/dist/esm/client/components/plan-pricing.js +156 -0
- package/dist/esm/client/components/plans-page.js +36 -0
- package/dist/esm/client/components/plans.js +209 -0
- package/dist/esm/client/components/readiness.js +113 -0
- package/dist/esm/client/components/subscribe-dialog.js +57 -0
- package/dist/esm/client/components/subscription.js +181 -0
- package/dist/esm/client/components/usage-meter.js +65 -0
- package/dist/esm/client/components/user-menu.js +164 -0
- package/dist/esm/client/index.js +47 -0
- package/dist/esm/client/mark.js +13 -0
- package/dist/esm/client/provider.js +247 -0
- package/dist/esm/client/use-checkout.js +168 -0
- package/dist/esm/errors.js +69 -0
- package/dist/esm/format-relative-time.js +15 -0
- package/dist/esm/helpers.js +26 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/next/index.js +279 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/server/client.js +110 -0
- package/dist/esm/server/handler.js +330 -0
- package/dist/esm/server/http.js +156 -0
- package/dist/esm/server/index.js +10 -0
- package/dist/esm/server/resources/access.js +24 -0
- package/dist/esm/server/resources/ai.js +32 -0
- package/dist/esm/server/resources/apps.js +26 -0
- package/dist/esm/server/resources/billing.js +67 -0
- package/dist/esm/server/resources/drive.js +35 -0
- package/dist/esm/server/resources/entitlements.js +45 -0
- package/dist/esm/server/resources/identity.js +189 -0
- package/dist/esm/server/resources/logs.js +37 -0
- package/dist/esm/server/resources/otp.js +25 -0
- package/dist/esm/server/resources/sessions.js +78 -0
- package/dist/esm/types.js +14 -0
- package/dist/theme.css +84 -0
- package/dist/types/cli/bin.d.ts +1 -0
- package/dist/types/cli/init.d.ts +34 -0
- package/dist/types/client/components/account-sessions.d.ts +25 -0
- package/dist/types/client/components/apps-grid.d.ts +20 -0
- package/dist/types/client/components/auth-buttons.d.ts +31 -0
- package/dist/types/client/components/feature-gate.d.ts +57 -0
- package/dist/types/client/components/link-button.d.ts +20 -0
- package/dist/types/client/components/logo.d.ts +35 -0
- package/dist/types/client/components/plan-pricing.d.ts +96 -0
- package/dist/types/client/components/plans-page.d.ts +36 -0
- package/dist/types/client/components/plans.d.ts +34 -0
- package/dist/types/client/components/readiness.d.ts +52 -0
- package/dist/types/client/components/subscribe-dialog.d.ts +34 -0
- package/dist/types/client/components/subscription.d.ts +123 -0
- package/dist/types/client/components/usage-meter.d.ts +24 -0
- package/dist/types/client/components/user-menu.d.ts +86 -0
- package/dist/types/client/index.d.ts +47 -0
- package/dist/types/client/mark.d.ts +11 -0
- package/dist/types/client/provider.d.ts +209 -0
- package/dist/types/client/use-checkout.d.ts +93 -0
- package/dist/types/errors.d.ts +62 -0
- package/dist/types/format-relative-time.d.ts +2 -0
- package/dist/types/helpers.d.ts +33 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/next/index.d.ts +136 -0
- package/dist/types/server/client.d.ts +83 -0
- package/dist/types/server/handler.d.ts +67 -0
- package/dist/types/server/http.d.ts +52 -0
- package/dist/types/server/index.d.ts +15 -0
- package/dist/types/server/resources/access.d.ts +17 -0
- package/dist/types/server/resources/ai.d.ts +24 -0
- package/dist/types/server/resources/apps.d.ts +12 -0
- package/dist/types/server/resources/billing.d.ts +41 -0
- package/dist/types/server/resources/drive.d.ts +26 -0
- package/dist/types/server/resources/entitlements.d.ts +31 -0
- package/dist/types/server/resources/identity.d.ts +76 -0
- package/dist/types/server/resources/logs.d.ts +20 -0
- package/dist/types/server/resources/otp.d.ts +17 -0
- package/dist/types/server/resources/sessions.d.ts +37 -0
- package/dist/types/types.d.ts +434 -0
- package/package.json +102 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
CosmoOps Proprietary License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CosmoOps. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software and its accompanying documentation (the "Software") are the
|
|
6
|
+
confidential and proprietary property of CosmoOps.
|
|
7
|
+
|
|
8
|
+
NO LICENSE IS GRANTED.
|
|
9
|
+
|
|
10
|
+
Except for CosmoOps and those it has authorised in writing, no person or
|
|
11
|
+
entity may, in whole or in part, for any purpose whether personal,
|
|
12
|
+
educational, non-commercial or commercial:
|
|
13
|
+
|
|
14
|
+
(a) use, run, execute or evaluate the Software;
|
|
15
|
+
(b) copy, reproduce, publish, distribute, sublicense, sell, rent, lease or
|
|
16
|
+
otherwise transfer the Software;
|
|
17
|
+
(c) modify, adapt, translate, or create derivative works of the Software;
|
|
18
|
+
(d) reverse engineer, decompile or disassemble the Software, except to the
|
|
19
|
+
extent that this restriction is prohibited by applicable law; or
|
|
20
|
+
(e) remove, obscure or alter this notice or any proprietary marking.
|
|
21
|
+
|
|
22
|
+
Publication of the Software to a public package registry is a distribution
|
|
23
|
+
convenience for CosmoOps and its authorised integrators. It is not an offer,
|
|
24
|
+
and it does not place the Software in the public domain, make it open source,
|
|
25
|
+
or grant any licence by implication, estoppel or otherwise.
|
|
26
|
+
|
|
27
|
+
To request authorisation, contact legal@cosmoops.com.
|
|
28
|
+
|
|
29
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
30
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
31
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
32
|
+
COSMOOPS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
|
33
|
+
ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
34
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
# @cosmoops/connect-sdk
|
|
2
|
+
|
|
3
|
+
Official client for the **CosmoOps Connect service-partner API** (`/api/v1/service/*`).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install @cosmoops/connect-sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Two entry points, and the split is a security boundary, not a preference:
|
|
10
|
+
|
|
11
|
+
| Import | Runs in | Holds the API key |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `@cosmoops/connect-sdk/server` | your backend | **yes** |
|
|
14
|
+
| `@cosmoops/connect-sdk/client` | the browser | never |
|
|
15
|
+
| `@cosmoops/connect-sdk/next` | your backend | **yes** (Next.js plug-and-play) |
|
|
16
|
+
| `@cosmoops/connect-sdk` | anywhere | types + `ConnectError` only |
|
|
17
|
+
|
|
18
|
+
The browser half never sees a credential. It calls your own origin, and
|
|
19
|
+
`createConnectHandler` — mounted in your backend — is what talks to Connect.
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
Browser ──► your backend ──► Connect
|
|
23
|
+
(client) (createConnectHandler + server client, holds the key)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Server setup
|
|
27
|
+
|
|
28
|
+
One credential does three jobs — the API key plaintext from your service's Setup tab is also your OAuth2 `client_secret` and the key your `id_token` is signed with. Revoking it revokes all three.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { ConnectClient } from "@cosmoops/connect-sdk/server";
|
|
32
|
+
|
|
33
|
+
export const connect = new ConnectClient({
|
|
34
|
+
apiKey: process.env.CONNECT_API_KEY!,
|
|
35
|
+
clientId: process.env.CONNECT_CLIENT_ID, // only needed for identity.*
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
An API key or a `sessionToken` in browser code is a leaked credential.
|
|
40
|
+
|
|
41
|
+
## Login with Connect
|
|
42
|
+
|
|
43
|
+
**1. Send the user out.**
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { createState } from "@cosmoops/connect-sdk/server";
|
|
47
|
+
|
|
48
|
+
const state = createState();
|
|
49
|
+
await saveToSession({ state });
|
|
50
|
+
|
|
51
|
+
redirect(connect.identity.authorizeUrl({
|
|
52
|
+
redirectUri: "https://you.example/callback",
|
|
53
|
+
state,
|
|
54
|
+
// prompt: "create" ← opens registration instead of sign-in
|
|
55
|
+
}));
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**2. Handle the callback.**
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
if (query.state !== savedState) throw new Error("state mismatch");
|
|
62
|
+
|
|
63
|
+
if (query.error === "login_required") return retrySignIn(); // normal — not an error
|
|
64
|
+
if (query.error) return showCancelled();
|
|
65
|
+
|
|
66
|
+
const session = await connect.identity.exchangeCode({
|
|
67
|
+
code: query.code,
|
|
68
|
+
redirectUri: "https://you.example/callback",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await connect.identity.verifyIdToken(session.idToken);
|
|
72
|
+
|
|
73
|
+
await saveToSession({ sessionToken: session.sessionToken });
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**3. On each later request.**
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
const { user, serviceRole } = await connect.identity.getUser(sessionToken);
|
|
80
|
+
|
|
81
|
+
if (!serviceRole.granted.includes("invoice:write")) return forbidden();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**4. On logout.**
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
await connect.identity.logout(sessionToken);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Gate on entitlements
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { blockingBlockers } from "@cosmoops/connect-sdk"; // isomorphic
|
|
94
|
+
|
|
95
|
+
const readiness = await connect.entitlements.getReadiness(orgId);
|
|
96
|
+
|
|
97
|
+
if (!readiness.ready) {
|
|
98
|
+
const [blocker] = blockingBlockers(readiness);
|
|
99
|
+
return showBlocked(blocker.message, blocker.resolveUrl); // resolveUrl links into Connect
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (await connect.entitlements.hasFeature(orgId, "campaign-module")) {
|
|
103
|
+
// ...
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Branch on `blocker.code`, never on `message`. Treat an unrecognised code as blocking — the list grows.
|
|
108
|
+
|
|
109
|
+
## Meter usage
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
const { balance } = await connect.billing.consumeAddon({
|
|
113
|
+
organizationId: orgId,
|
|
114
|
+
addonId: "api-calls",
|
|
115
|
+
quantity: 1,
|
|
116
|
+
});
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
An `Idempotency-Key` is sent on every consume, so a retry can't double-debit.
|
|
120
|
+
|
|
121
|
+
## Errors
|
|
122
|
+
|
|
123
|
+
Everything throws `ConnectError` with a stable `code`.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
import { isConnectError } from "@cosmoops/connect-sdk"; // isomorphic
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await connect.identity.getUser(sessionToken);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
if (isConnectError(error) && error.isSessionExpired) return redirectToSignIn();
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`isRetryable` covers throttling, 5xx and network failures. The SDK already retries those for safe requests; the flag is for deciding whether to queue your own.
|
|
137
|
+
|
|
138
|
+
## Namespaces
|
|
139
|
+
|
|
140
|
+
| | |
|
|
141
|
+
| --- | --- |
|
|
142
|
+
| `identity` | `authorizeUrl` · `exchangeCode` · `verifyIdToken` · `getUser` · `logout` |
|
|
143
|
+
| `access` | `getUserRoles` · `listRoles` |
|
|
144
|
+
| `entitlements` | `getReadiness` · `getSubscription` · `hasFeature` |
|
|
145
|
+
| `billing` | `listPlans` · `getAddonUsage` · `consumeAddon` |
|
|
146
|
+
| `drive` | `upload` · `getFileUrl` · `getPreviewUrl` |
|
|
147
|
+
| `ai` | `chat` · `extract` |
|
|
148
|
+
| `otp` | `send` · `verify` |
|
|
149
|
+
| `logs` | `write` |
|
|
150
|
+
| `apps` | `list` |
|
|
151
|
+
| `sessions` | `list` · `revoke` · `revokeOthers` |
|
|
152
|
+
|
|
153
|
+
### Attributing a log to a user
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
await client.logs.write({
|
|
157
|
+
message: "campaign sent",
|
|
158
|
+
metadata: { campaignId },
|
|
159
|
+
sessionToken, // that user's Connect session token
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Both scopes on the stored entry come from something verified, never from your payload: `serviceId` from your API key, and `userId` from this token — which Connect checks is unrevoked, unexpired, and **minted for your service** before attributing anything. **There is no `userId` field to send.** An actor a caller can type is not an actor worth recording, and a conformance test fails the build if one ever appears in the server schema.
|
|
164
|
+
|
|
165
|
+
`write` returns `{ delivered, attributed }` and never throws. `attributed: false` with `delivered: true` means the token had expired (~15 days) or been revoked and the line was stored *without* a user — deliberately kept rather than rejected, because losing telemetry over a failed attribution is the worse of the two outcomes.
|
|
166
|
+
|
|
167
|
+
## Options
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
new ConnectClient({
|
|
171
|
+
apiKey,
|
|
172
|
+
clientId,
|
|
173
|
+
baseUrl, // default https://connect.cosmoops.com
|
|
174
|
+
timeoutMs, // default 20000, per attempt
|
|
175
|
+
maxRetries, // default 2, safe requests only
|
|
176
|
+
fetch, // bring your own instrumented fetch
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Next.js — plug and play
|
|
181
|
+
|
|
182
|
+
Two files, no arguments.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
// app/api/connect/[...connect]/route.ts
|
|
186
|
+
export { GET, POST } from "@cosmoops/connect-sdk/next";
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```tsx
|
|
190
|
+
// app/layout.tsx
|
|
191
|
+
import { ConnectProvider } from "@cosmoops/connect-sdk/client";
|
|
192
|
+
|
|
193
|
+
<ConnectProvider>{children}</ConnectProvider>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
```css
|
|
197
|
+
/* app/globals.css */
|
|
198
|
+
@import "tailwindcss";
|
|
199
|
+
@import "@heroui/styles";
|
|
200
|
+
@import "@cosmoops/connect-sdk/theme.css";
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`npx @cosmoops/connect-sdk init` writes the route file and a `/plans` page for
|
|
204
|
+
you; the provider and the stylesheet are the two it won't edit on your behalf.
|
|
205
|
+
|
|
206
|
+
That gives you the whole round trip — `/signin`, `/callback`, an httpOnly session
|
|
207
|
+
cookie, `/session`, `/entitlements`, `/plans`, `/addons`, `/apps`, `/sessions`,
|
|
208
|
+
`/logout`. Config comes from the environment:
|
|
209
|
+
|
|
210
|
+
| Variable | Required | Purpose |
|
|
211
|
+
| --- | --- | --- |
|
|
212
|
+
| `CONNECT_API_KEY` | yes | server-side credential |
|
|
213
|
+
| `CONNECT_CLIENT_ID` | yes | OAuth client id |
|
|
214
|
+
| `NEXT_PUBLIC_CONNECT_CLIENT_ID` | yes | same value, for `<ConnectProvider>` |
|
|
215
|
+
| `CONNECT_APP_URL` | no | non-production Connect instance |
|
|
216
|
+
| `CONNECT_SESSION_COOKIE` | no | session cookie name |
|
|
217
|
+
|
|
218
|
+
Register `{your origin}/api/connect/callback` as a redirect URI in the service's
|
|
219
|
+
Setup tab — the one step this cannot do for you.
|
|
220
|
+
|
|
221
|
+
Optional `next.config` wrapper, which warns at config load if the public client
|
|
222
|
+
id is missing (a `NEXT_PUBLIC_*` that isn't set fails silently in the browser):
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import { withConnect } from "@cosmoops/connect-sdk/next";
|
|
226
|
+
export default withConnect({ });
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
That is all it does. It does **not** stand in for the route file — Next has no
|
|
230
|
+
plugin API that contributes a route from a package, so
|
|
231
|
+
`app/api/connect/[...connect]/route.ts` has to exist either way. The two are
|
|
232
|
+
independent; using both is just the default setup.
|
|
233
|
+
|
|
234
|
+
Reading the session server-side — no argument, no `headers()` dance:
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
// app/dashboard/page.tsx
|
|
238
|
+
import { getConnectSession } from "@cosmoops/connect-sdk/next";
|
|
239
|
+
import { redirect } from "next/navigation";
|
|
240
|
+
|
|
241
|
+
export default async function DashboardPage() {
|
|
242
|
+
const session = await getConnectSession(); // UserResponse | null
|
|
243
|
+
if (!session) redirect("/api/connect/signin?next=/dashboard");
|
|
244
|
+
return <h1>{session.user.organizationName}</h1>;
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Pass a `Request` — `getConnectSession(request)` — anywhere `next/headers` does
|
|
249
|
+
not exist: a framework-agnostic handler, a custom server, a test.
|
|
250
|
+
|
|
251
|
+
It reads `cookies()` transitively, so under `cacheComponents` it belongs inside
|
|
252
|
+
a `<Suspense>` boundary like any other request-scoped read. A page that awaits
|
|
253
|
+
it above every boundary gives up its static shell.
|
|
254
|
+
|
|
255
|
+
You do **not** need `transpilePackages`. `dist/` is ES2022 with `"use client"`
|
|
256
|
+
already hoisted, so Turbopack and webpack consume it as-is.
|
|
257
|
+
|
|
258
|
+
## Other frameworks
|
|
259
|
+
|
|
260
|
+
Mount the bridge once in your backend:
|
|
261
|
+
|
|
262
|
+
```ts
|
|
263
|
+
// app/api/connect/[...route]/route.ts
|
|
264
|
+
import { createConnectHandler } from "@cosmoops/connect-sdk/server";
|
|
265
|
+
import { connect } from "@/lib/connect";
|
|
266
|
+
|
|
267
|
+
const handler = createConnectHandler({
|
|
268
|
+
client: connect,
|
|
269
|
+
session: {
|
|
270
|
+
read: async (req) => (await cookies()).get("connect_session")?.value ?? null,
|
|
271
|
+
clear: (req, res) => res.headers.append("set-cookie", "connect_session=; Max-Age=0; Path=/"),
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
export { handler as GET, handler as POST };
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Then wrap your app:
|
|
279
|
+
|
|
280
|
+
```tsx
|
|
281
|
+
import { ConnectProvider } from "@cosmoops/connect-sdk/client";
|
|
282
|
+
|
|
283
|
+
<ConnectProvider clientId={process.env.NEXT_PUBLIC_CONNECT_CLIENT_ID!}>
|
|
284
|
+
{children}
|
|
285
|
+
</ConnectProvider>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Components
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
import {
|
|
292
|
+
ConnectLoginButton, ConnectRegisterButton,
|
|
293
|
+
ConnectUserMenu, ConnectAppsGrid, ConnectPlans,
|
|
294
|
+
} from "@cosmoops/connect-sdk/client";
|
|
295
|
+
|
|
296
|
+
// Top bar — the pair swaps automatically on sign-in state
|
|
297
|
+
<ConnectAppsGrid />
|
|
298
|
+
<ConnectLoginButton />
|
|
299
|
+
<ConnectUserMenu />
|
|
300
|
+
|
|
301
|
+
// Pricing page
|
|
302
|
+
<ConnectPlans />
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
| Component | What it does |
|
|
306
|
+
| --- | --- |
|
|
307
|
+
| `ConnectLoginButton` · `ConnectRegisterButton` | "Continue with Connect", in the shape people know from Google. Render nothing when signed in. |
|
|
308
|
+
| `ConnectUserMenu` | Avatar, name, workspace. Profile, Manage subscription and Open Connect each open Connect in a new tab; Sign out acts in place. Renders nothing when signed out. |
|
|
309
|
+
| `ConnectAppsGrid` | Google-style launcher — grid icon opening a modal of every app this workspace can open. |
|
|
310
|
+
| `ConnectPlans` | Monthly/annual toggle, features, add-ons, live discount, coupon field, and the subscribe / upgrade / downgrade / cancel flow. |
|
|
311
|
+
| `ConnectPlansPage` | A whole `/plans` page — readiness banner, current subscription, and the grid. What `connect-sdk init` scaffolds. |
|
|
312
|
+
| `ConnectSubscribeDialog` | The review step `ConnectPlans` opens — plan, GST, what happens to the current cycle. Exported so you can drive the flow from your own pricing page. |
|
|
313
|
+
| `ConnectSubscriptionGate` | Renders your module for a subscribed workspace, and `ConnectNoSubscriptionCard` for everyone else. Never flashes the upsell while loading. |
|
|
314
|
+
| `ConnectNoSubscriptionCard` · `ConnectSubscriptionCard` | The same slot, unsubscribed and subscribed. Plan, status, renewal date, seats, and one way into Connect. |
|
|
315
|
+
| `ConnectReadinessBanner` · `ConnectReadinessGate` | Every gate between this workspace and your service — KYC, payment method, seat headroom — each with the Connect screen that fixes it. |
|
|
316
|
+
| `ConnectFeatureGate` | Renders a module only when `plan.features` includes its key. |
|
|
317
|
+
| `ConnectTrialBanner` | A running trial and the date it converts. Renders nothing otherwise. |
|
|
318
|
+
| `ConnectAccountSessions` | Every device signed into this Connect account, with revoke. |
|
|
319
|
+
| `ConnectUsageMeter` | Remaining balance of each consumable add-on, with overage and carry-forward. |
|
|
320
|
+
| `ConnectManageSubscriptionButton` · `ConnectInvoicesButton` · `ConnectProfileButton` · `ConnectHomeButton` · `ConnectSignOutButton` | The menu items as standalone buttons — Connect's Billing, Invoices and Profile. |
|
|
321
|
+
| `ConnectLogo` | The shared lockup — your `icon` on the CosmoOps badge, `cosmo·ops` beside it, your `name` underneath. |
|
|
322
|
+
|
|
323
|
+
### Hooks
|
|
324
|
+
|
|
325
|
+
`useConnect` · `useConnectUser` · `useConnectEntitlements` · `useConnectPlans` · `useConnectApps` · `useConnectAddonUsage` · `useConnectSessions` · `useConnectResource` · `useConnectFeature` · `useConnectCheckout`
|
|
326
|
+
|
|
327
|
+
### Metering
|
|
328
|
+
|
|
329
|
+
```tsx
|
|
330
|
+
await client.billing.consumeAddon({ organizationId, addonId: "addon_sms", quantity: 1 }); // server
|
|
331
|
+
<ConnectUsageMeter /> // browser
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
The meter reads the same balance `consumeAddon` debits (`addons:read`, separate from `addons:consume` so a key that only renders a meter cannot spend the balance down). It shows `balance` against `includedPerCycle` and never a "used" figure: with `carryForward` a balance survives the cycle boundary, so `included − balance` can go negative — a meter running backwards.
|
|
335
|
+
|
|
336
|
+
Invoices stay a deep link (`ConnectInvoicesButton`). A GST tax invoice is a legal document emailed and downloaded from one place; a partner-rendered copy would be a second version of a record that must have exactly one.
|
|
337
|
+
|
|
338
|
+
### Plan features: label vs key
|
|
339
|
+
|
|
340
|
+
The catalogue read (`useConnectPlans`, `<ConnectPlans />`) reports `features` as `{ key, name, description }` — **render `name`, gate on `key`**. The subscription read (`useConnectEntitlements`, `hasFeature`, `<ConnectFeatureGate>`) reports `features` as a bare `string[]` of keys, and stays that way: that one is a contract your backend compares against, and a display label is neither stable nor comparable. `name` falls back to the key for a plan older than its service's feature catalog.
|
|
341
|
+
|
|
342
|
+
### Gates
|
|
343
|
+
|
|
344
|
+
```tsx
|
|
345
|
+
<ConnectReadinessBanner /> {/* KYC, payment method, seats — each with a fix */}
|
|
346
|
+
<ConnectSubscriptionGate><Dashboard /></ConnectSubscriptionGate>
|
|
347
|
+
<ConnectFeatureGate feature="advanced_reports"><Reports /></ConnectFeatureGate>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
All three render `loading` — never the fallback — while the answer is unknown, so a paying customer is never flashed an upsell for something they already have.
|
|
351
|
+
|
|
352
|
+
**They are UX gates, not security boundaries.** Everything they read reaches your own origin and a browser can ignore it. The authoritative check is on your server: `client.entitlements.getReadiness(orgId)` and `client.entitlements.hasFeature(orgId, feature)`.
|
|
353
|
+
|
|
354
|
+
### Scaffolding
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
npx @cosmoops/connect-sdk init
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Writes `app/api/connect/[...connect]/route.ts`, `app/plans/page.tsx` and an `.env.local` template, then prints the two steps it will not do for you (wrapping your layout in `<ConnectProvider>`, and registering the redirect URI in Connect). It never overwrites, so re-running is safe.
|
|
361
|
+
|
|
362
|
+
Next has no plugin API that adds routes — `next.config` can rewrite and redirect, but both need a route to point at, and the App Router only discovers routes in your own `app/` directory. So this is a scaffolder rather than a plugin: `ConnectPlansPage` makes the file that must exist one line, and `init` writes that line.
|
|
363
|
+
|
|
364
|
+
### Subscribe, upgrade, downgrade, cancel
|
|
365
|
+
|
|
366
|
+
`<ConnectPlans />` runs all three. Pressing a plan's action opens a review step — the plan, its GST-inclusive price, and what happens to the cycle the customer is part-way through — and confirming opens Connect on the confirmation screen **for that exact plan**, not a catalogue they have to search again. The page then watches `/entitlements` and repaints itself once the change lands, so nobody has to be told to refresh.
|
|
367
|
+
|
|
368
|
+
To drive it from your own pricing page instead, the two pieces are exported:
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
import { useConnectCheckout, buildCheckoutUrl, useConnect, useConnectEntitlements }
|
|
372
|
+
from "@cosmoops/connect-sdk/client";
|
|
373
|
+
|
|
374
|
+
const { appUrl } = useConnect();
|
|
375
|
+
const { data } = useConnectEntitlements();
|
|
376
|
+
const checkout = useConnectCheckout();
|
|
377
|
+
|
|
378
|
+
<button
|
|
379
|
+
onClick={() =>
|
|
380
|
+
checkout.start({
|
|
381
|
+
url: buildCheckoutUrl({
|
|
382
|
+
appUrl,
|
|
383
|
+
serviceId: data!.subscription.serviceId,
|
|
384
|
+
planId: "plan_growth_m",
|
|
385
|
+
// Present ⇒ change this subscription's plan; absent ⇒ first subscribe.
|
|
386
|
+
subscriptionId: data!.subscription.subscription?.id ?? null,
|
|
387
|
+
}),
|
|
388
|
+
isSettled: (next) => next.subscription.plan?.id === "plan_growth_m",
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
>
|
|
392
|
+
Upgrade
|
|
393
|
+
</button>
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
`checkout.status` moves `idle → waiting → completed | abandoned`, plus `blocked` if the browser refused the window (render `checkout.url` as a link). **A closed window is never read as success** — only the entitlement changing is, because "paid and closed it" and "opened it and thought better of it" look identical from here.
|
|
397
|
+
|
|
398
|
+
Cancelling uses the same machinery via `buildCancelUrl`. There is no *resume*: Connect has no uncancel path, so a subscription already set to cancel gets a statement of when access ends, not a button that cannot work.
|
|
399
|
+
|
|
400
|
+
### Logo
|
|
401
|
+
|
|
402
|
+
```tsx
|
|
403
|
+
import { ConnectLogo } from "@cosmoops/connect-sdk/client";
|
|
404
|
+
import { Truck } from "lucide-react";
|
|
405
|
+
|
|
406
|
+
<ConnectLogo name="Dispatch" icon={<Truck />} />
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Each platform passes only what differs — `name` and `icon`. Mark geometry, wordmark, separator and accent come from one place, so the family reads as one product. Sizes are `em`-based: `className="text-xl"` scales the whole lockup, which is why there's no `size` prop. `collapsed` gives the mark alone.
|
|
410
|
+
|
|
411
|
+
### Theming
|
|
412
|
+
|
|
413
|
+
```css
|
|
414
|
+
@import "tailwindcss";
|
|
415
|
+
@import "@heroui/styles";
|
|
416
|
+
@import "@cosmoops/connect-sdk/theme.css";
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
One shared **HeroUI v3** theme for every service platform. The components paint with semantic tokens only — `accent`, `muted`, `surface`, `separator`, `danger` — never a hardcoded colour, so one edit to the theme restyles all of them everywhere at once. To diverge on brand, redefine individual tokens *after* the import rather than forking the file.
|
|
420
|
+
|
|
421
|
+
Requires `@heroui/react` and `lucide-react` as peers (optional — server-only consumers need neither).
|
|
422
|
+
|
|
423
|
+
### Why payment happens on Connect
|
|
424
|
+
|
|
425
|
+
The *flow* is in your app; the *payment* is not. A plan change is not one write — it re-runs the readiness gates, prices proration against accumulated credit, re-checks the RBI mandate ceiling for the new amount, and issues a GST invoice. Connect owns that sequence end to end, and a partner-side implementation would be a second copy of it that drifts. It would also mean a browser holding your credentials could move money, which is the one thing the bridge exists to prevent.
|
|
426
|
+
|
|
427
|
+
So the hand-off is a URL, and Connect renders it as a modal over the relevant page (an intercepted parallel route) — the customer lands on the confirmation for the plan they picked, and returns to a subscription that is already correct.
|
|
428
|
+
|
|
429
|
+
## Development
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
bun test # server suite - hermetic, no network
|
|
433
|
+
bun run storybook # client components against a mock bridge, port 6106
|
|
434
|
+
bun run build # dual CJS/ESM + types
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Server tests drive the injectable `fetch` seam, so nothing reaches Connect.
|
|
438
|
+
Storybook renders the real `ConnectProvider` and substitutes only `fetch` — the
|
|
439
|
+
same seam the production bridge sits behind — so loading, error and empty
|
|
440
|
+
states are exercised for real. `.storybook/theme.css` is a sample host theme:
|
|
441
|
+
change it and every story should move together. If one doesn't, it has a
|
|
442
|
+
hardcoded colour.
|
|
443
|
+
|
|
444
|
+
## License
|
|
445
|
+
|
|
446
|
+
Proprietary. See [LICENSE](./LICENSE) — no rights are granted to anyone outside CosmoOps and its authorised integrators.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
/**
|
|
5
|
+
* The `connect-sdk` executable.
|
|
6
|
+
*
|
|
7
|
+
* A thin wrapper so `init.ts` stays importable and testable - a module that
|
|
8
|
+
* calls `process.exit` at import time cannot be unit tested, and this is the
|
|
9
|
+
* only file that does.
|
|
10
|
+
*
|
|
11
|
+
* The shebang is added by `scripts/finalize-dist.ts`: TypeScript strips leading
|
|
12
|
+
* comments it does not recognise as directives, and a `bin` without one is run
|
|
13
|
+
* by the shell rather than by Node.
|
|
14
|
+
*/
|
|
15
|
+
const init_js_1 = require("./init.js");
|
|
16
|
+
process.exit((0, init_js_1.main)(process.argv.slice(2)));
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* `npx @cosmoops/connect-sdk init` - writes the Connect wiring into a Next.js
|
|
5
|
+
* App Router project.
|
|
6
|
+
*
|
|
7
|
+
* ## Why this is a CLI and not a plugin
|
|
8
|
+
*
|
|
9
|
+
* Next has no plugin API that injects routes. `next.config` can rewrite and
|
|
10
|
+
* redirect, but both need a real route to point at, and there is no hook that
|
|
11
|
+
* contributes a `page.tsx` from a node_module - the App Router discovers routes
|
|
12
|
+
* by walking the project's own `app/` directory and nothing else. So a package
|
|
13
|
+
* cannot add `/plans` to an app by being installed, however much one would like
|
|
14
|
+
* it to.
|
|
15
|
+
*
|
|
16
|
+
* What it *can* do is make the file that must exist a single line, and then
|
|
17
|
+
* write that file for you. `ConnectPlansPage` is the first half; this is the
|
|
18
|
+
* second. The result is the same as a plugin from the developer's side - one
|
|
19
|
+
* command and the page is there - without pretending an extension point exists.
|
|
20
|
+
*
|
|
21
|
+
* Nothing here overwrites. Every file that already exists is reported as
|
|
22
|
+
* skipped, so re-running after a partial setup is safe and running it on an
|
|
23
|
+
* app that already has Connect wired up does nothing at all.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.runInit = runInit;
|
|
27
|
+
exports.main = main;
|
|
28
|
+
const node_fs_1 = require("node:fs");
|
|
29
|
+
const node_path_1 = require("node:path");
|
|
30
|
+
const BRIDGE_ROUTE = `// The Connect bridge - the only place your API key is used.
|
|
31
|
+
//
|
|
32
|
+
// It answers your own origin, so the browser half of the SDK never holds a
|
|
33
|
+
// credential. GET and POST are the same function; the bridge routes on method
|
|
34
|
+
// and path itself.
|
|
35
|
+
export { GET, POST } from "@cosmoops/connect-sdk/next";
|
|
36
|
+
`;
|
|
37
|
+
const PLANS_PAGE = `"use client";
|
|
38
|
+
|
|
39
|
+
// The full pricing page: readiness banner, current subscription, plan grid, and
|
|
40
|
+
// the subscribe / upgrade / downgrade flow.
|
|
41
|
+
//
|
|
42
|
+
// Swap this for \`<ConnectPlansPage title="..." />\` (or compose \`ConnectPlans\`
|
|
43
|
+
// with the other exports) whenever you want your own copy or layout.
|
|
44
|
+
export { ConnectPlansPage as default } from "@cosmoops/connect-sdk/client";
|
|
45
|
+
`;
|
|
46
|
+
const ENV_TEMPLATE = `# CosmoOps Connect - values come from your service's Setup tab.
|
|
47
|
+
CONNECT_API_KEY=
|
|
48
|
+
CONNECT_CLIENT_ID=
|
|
49
|
+
# Same value as CONNECT_CLIENT_ID. Inlined at build time for <ConnectProvider>.
|
|
50
|
+
NEXT_PUBLIC_CONNECT_CLIENT_ID=
|
|
51
|
+
`;
|
|
52
|
+
const SCAFFOLDS = [
|
|
53
|
+
{
|
|
54
|
+
path: (0, node_path_1.join)("api", "connect", "[...connect]", "route.ts"),
|
|
55
|
+
contents: BRIDGE_ROUTE,
|
|
56
|
+
description: "Connect bridge route handler",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
path: (0, node_path_1.join)("plans", "page.tsx"),
|
|
60
|
+
contents: PLANS_PAGE,
|
|
61
|
+
description: "/plans pricing page",
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
/**
|
|
65
|
+
* `app/` or `src/app/`, whichever this project uses.
|
|
66
|
+
*
|
|
67
|
+
* Both are official layouts and neither is more correct, so guessing wrong
|
|
68
|
+
* would scatter routes into a directory Next never reads - a failure that looks
|
|
69
|
+
* like "the page just 404s" and takes a while to trace. Refusing to guess is
|
|
70
|
+
* better than a default.
|
|
71
|
+
*/
|
|
72
|
+
function findAppDir(root) {
|
|
73
|
+
for (const candidate of [(0, node_path_1.join)(root, "app"), (0, node_path_1.join)(root, "src", "app")]) {
|
|
74
|
+
if ((0, node_fs_1.existsSync)(candidate))
|
|
75
|
+
return candidate;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function write(file, contents) {
|
|
80
|
+
if ((0, node_fs_1.existsSync)(file))
|
|
81
|
+
return "skipped";
|
|
82
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(file), { recursive: true });
|
|
83
|
+
(0, node_fs_1.writeFileSync)(file, contents);
|
|
84
|
+
return "written";
|
|
85
|
+
}
|
|
86
|
+
/** Appends the Connect block to `.env.local`, or creates it - never rewrites an existing value. */
|
|
87
|
+
function ensureEnv(root) {
|
|
88
|
+
const file = (0, node_path_1.join)(root, ".env.local");
|
|
89
|
+
if (!(0, node_fs_1.existsSync)(file)) {
|
|
90
|
+
(0, node_fs_1.writeFileSync)(file, ENV_TEMPLATE);
|
|
91
|
+
return "written";
|
|
92
|
+
}
|
|
93
|
+
const current = (0, node_fs_1.readFileSync)(file, "utf8");
|
|
94
|
+
if (current.includes("CONNECT_CLIENT_ID"))
|
|
95
|
+
return "skipped";
|
|
96
|
+
(0, node_fs_1.writeFileSync)(file, `${current.trimEnd()}\n\n${ENV_TEMPLATE}`);
|
|
97
|
+
return "appended";
|
|
98
|
+
}
|
|
99
|
+
function runInit(argv = []) {
|
|
100
|
+
const root = (0, node_path_1.resolve)(argv[0] ?? process.cwd());
|
|
101
|
+
const appDir = findAppDir(root);
|
|
102
|
+
if (!appDir) {
|
|
103
|
+
console.error(`[connect-sdk] No App Router directory found in ${root} - looked for "app/" and "src/app/".\n` +
|
|
104
|
+
"Run this from your Next.js project root, or pass the path: npx @cosmoops/connect-sdk init ./my-app");
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
console.log(`[connect-sdk] Wiring Connect into ${(0, node_path_1.relative)(process.cwd(), appDir) || appDir}\n`);
|
|
108
|
+
for (const scaffold of SCAFFOLDS) {
|
|
109
|
+
const target = (0, node_path_1.join)(appDir, scaffold.path);
|
|
110
|
+
const result = write(target, scaffold.contents);
|
|
111
|
+
const marker = result === "written" ? "created" : "exists ";
|
|
112
|
+
console.log(` ${marker} ${(0, node_path_1.relative)(root, target)} - ${scaffold.description}`);
|
|
113
|
+
}
|
|
114
|
+
const env = ensureEnv(root);
|
|
115
|
+
console.log(` ${env === "skipped" ? "exists " : env === "appended" ? "updated" : "created"} .env.local - fill in the three values from your service's Setup tab`);
|
|
116
|
+
// The two steps a scaffolder must not do for you: editing an existing layout
|
|
117
|
+
// means parsing and rewriting JSX someone else wrote, and the redirect URI
|
|
118
|
+
// lives in Connect, not in this repo.
|
|
119
|
+
console.log(`
|
|
120
|
+
Three things left, all one-time:
|
|
121
|
+
|
|
122
|
+
1. Wrap your app in the provider - app/layout.tsx:
|
|
123
|
+
|
|
124
|
+
import { ConnectProvider } from "@cosmoops/connect-sdk/client";
|
|
125
|
+
|
|
126
|
+
<ConnectProvider>{children}</ConnectProvider>
|
|
127
|
+
|
|
128
|
+
No props: it reads NEXT_PUBLIC_CONNECT_CLIENT_ID.
|
|
129
|
+
|
|
130
|
+
2. Add the theme to your global stylesheet, last of the three - Tailwind
|
|
131
|
+
first, HeroUI's token layer next, so these values win:
|
|
132
|
+
|
|
133
|
+
@import "tailwindcss";
|
|
134
|
+
@import "@heroui/styles";
|
|
135
|
+
@import "@cosmoops/connect-sdk/theme.css";
|
|
136
|
+
|
|
137
|
+
It has to be imported from your *own* CSS, not from node_modules, or
|
|
138
|
+
Tailwind roots its content scan inside node_modules and skips your app.
|
|
139
|
+
|
|
140
|
+
3. Register {your origin}/api/connect/callback as a redirect URI in your
|
|
141
|
+
service's Setup tab in Connect.
|
|
142
|
+
|
|
143
|
+
Optionally, in next.config.ts:
|
|
144
|
+
|
|
145
|
+
import { withConnect } from "@cosmoops/connect-sdk/next";
|
|
146
|
+
export default withConnect({ });
|
|
147
|
+
|
|
148
|
+
It checks NEXT_PUBLIC_CONNECT_CLIENT_ID at config load - the one variable whose
|
|
149
|
+
absence fails silently in the browser rather than loudly on the server.
|
|
150
|
+
|
|
151
|
+
Then <ConnectPlans />, <ConnectSubscriptionGate>, <ConnectFeatureGate> and the
|
|
152
|
+
rest work anywhere below the provider, and getConnectSession() reads the same
|
|
153
|
+
session from any Server Component.
|
|
154
|
+
`);
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Only `init` today. Parsed as a subcommand anyway so adding `doctor` or
|
|
159
|
+
* `check` later does not change how the existing one is invoked.
|
|
160
|
+
*
|
|
161
|
+
* A bare path runs `init` against it, so both `connect-sdk init ./my-app` and
|
|
162
|
+
* `connect-sdk ./my-app` work. A first argument that is neither the command nor
|
|
163
|
+
* an existing directory is a typo, and saying so beats reporting that
|
|
164
|
+
* `./innit` contains no App Router - which is true, unhelpful, and points at
|
|
165
|
+
* the wrong mistake.
|
|
166
|
+
*/
|
|
167
|
+
function main(argv) {
|
|
168
|
+
const [first, ...rest] = argv;
|
|
169
|
+
if (first === "--help" || first === "-h") {
|
|
170
|
+
console.log("Usage: connect-sdk init [project-dir]");
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
if (first === "init")
|
|
174
|
+
return runInit(rest);
|
|
175
|
+
if (!first || (0, node_fs_1.existsSync)(first))
|
|
176
|
+
return runInit(argv);
|
|
177
|
+
console.error(`[connect-sdk] Unknown command "${first}". Try: connect-sdk init`);
|
|
178
|
+
return 1;
|
|
179
|
+
}
|