@topolo/cli 0.1.16 → 0.1.18
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/README.md +20 -4
- package/dist/index.js +474 -98
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ via `stdin`/`stdout`.
|
|
|
9
9
|
- API-key authentication (long-lived, service-scoped).
|
|
10
10
|
- Access-token authentication via OAuth device flow when a CLI OAuth client id is configured.
|
|
11
11
|
- Stored access-token refresh when the config includes a refresh token and OAuth client id.
|
|
12
|
+
- First-class `production` and `staging` CLI environments with separate stored credentials, OAuth client ids, and service URL overrides.
|
|
12
13
|
- Commands: `auth login/logout/status`, `whoami`, `apps`, `apps scaffold`, `services`, `crm contacts:list`, `crm contacts:get`, and generic `api` passthrough for SDK-registered services.
|
|
13
14
|
|
|
14
15
|
## Install
|
|
@@ -23,9 +24,19 @@ npx @topolo/cli whoami
|
|
|
23
24
|
|
|
24
25
|
Credentials come from (in priority order):
|
|
25
26
|
|
|
26
|
-
1. `
|
|
27
|
-
2. `
|
|
28
|
-
3.
|
|
27
|
+
1. Environment-specific vars such as `TOPOLO_STAGING_API_KEY` or `TOPOLO_PRODUCTION_ACCESS_TOKEN`
|
|
28
|
+
2. Global vars `TOPOLO_API_KEY` or `TOPOLO_ACCESS_TOKEN`
|
|
29
|
+
3. The selected environment profile in the config file (`$XDG_CONFIG_HOME/topolo/config.json`, mode `0600`)
|
|
30
|
+
|
|
31
|
+
Production is the default. Select staging with `--env staging` anywhere in the command, or set `TOPOLO_ENV=staging`:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
topolo auth status --env staging --json
|
|
35
|
+
topolo auth login --env staging --client-id doac_...
|
|
36
|
+
topolo platform apps scaffold home --env staging --write --json
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The staging profile uses `auth.stg.topolo.us`, `developers.stg.topolo.us`, and `seed.stg.topolo.us` by default. Other SDK service calls in staging must have an explicit staging URL override; the CLI refuses to call a production SDK URL while `--env staging` is selected.
|
|
29
40
|
|
|
30
41
|
For interactive use:
|
|
31
42
|
|
|
@@ -38,7 +49,8 @@ topolo auth logout # clears the stored credential
|
|
|
38
49
|
|
|
39
50
|
If no client id is configured, `topolo auth login` falls back to an API-key
|
|
40
51
|
prompt in an interactive terminal. Non-interactive sessions should pass
|
|
41
|
-
`--api-key` or set `
|
|
52
|
+
`--api-key` or set `TOPOLO_PRODUCTION_CLI_CLIENT_ID`,
|
|
53
|
+
`TOPOLO_STAGING_CLI_CLIENT_ID`, or the shared `TOPOLO_CLI_CLIENT_ID`.
|
|
42
54
|
|
|
43
55
|
## Cross-org isolation
|
|
44
56
|
|
|
@@ -97,9 +109,13 @@ topolo crm contacts:get contact_abc123
|
|
|
97
109
|
### `topolo services`
|
|
98
110
|
|
|
99
111
|
Lists every service id and effective URL known to the generated SDK registry.
|
|
112
|
+
In staging, `environmentSafe: false` means that service still resolves to a
|
|
113
|
+
production SDK default and will be refused by `topolo api` until a staging URL
|
|
114
|
+
override is configured.
|
|
100
115
|
|
|
101
116
|
```bash
|
|
102
117
|
topolo services --json
|
|
118
|
+
topolo services --env staging --json
|
|
103
119
|
```
|
|
104
120
|
|
|
105
121
|
### `topolo apps`
|
package/dist/index.js
CHANGED
|
@@ -75,13 +75,16 @@ var SERVICES = {
|
|
|
75
75
|
chat: "https://chat.topolo.app",
|
|
76
76
|
commerce: "https://commerce-api.topolo.app",
|
|
77
77
|
compose: "https://compose.topolo.app",
|
|
78
|
+
consent: "https://consent.topolo.app",
|
|
78
79
|
crm: "https://crm-api.topolo.app",
|
|
79
80
|
customer_success: "https://success.topolo.app",
|
|
81
|
+
developers: "https://developers.topolo.app",
|
|
80
82
|
director: "https://director.topolo.app",
|
|
81
83
|
feed: "https://feed-api.topolo.app",
|
|
82
84
|
flow: "https://flow.topolo.app",
|
|
83
85
|
forecast: "https://forecast-api.topolo.app",
|
|
84
86
|
forms: "https://forms.topolo.app",
|
|
87
|
+
home: "https://home.topolo.app",
|
|
85
88
|
insights: "https://insights.topolo.app",
|
|
86
89
|
inventory: "https://inventory.topolo.app",
|
|
87
90
|
learn: "https://learn-api.topolo.app",
|
|
@@ -112,11 +115,14 @@ var PLATFORM_SERVICE_IDS = {
|
|
|
112
115
|
books: "srv_topolo_books",
|
|
113
116
|
bugfix: "srv_topolo_bugfix",
|
|
114
117
|
compose: "srv_topolo_compose",
|
|
118
|
+
consent: "srv_topolo_consent",
|
|
115
119
|
crm: "srv_iCwM4jGXcwlj",
|
|
116
120
|
customer_success: "srv_topolo_success",
|
|
121
|
+
developers: "svc_topolo_developers",
|
|
117
122
|
director: "srv_topolo_director",
|
|
118
123
|
flow: "srv_topolo_flow",
|
|
119
124
|
forms: "srv_topolo_forms",
|
|
125
|
+
home: "srv_topolo_home",
|
|
120
126
|
insights: "srv_topolo_insights",
|
|
121
127
|
inventory: "srv_topolo_inventory",
|
|
122
128
|
localize: "srv_topolo_localize",
|
|
@@ -449,6 +455,30 @@ var APPLICATIONS = {
|
|
|
449
455
|
}
|
|
450
456
|
]
|
|
451
457
|
},
|
|
458
|
+
"consent": {
|
|
459
|
+
"id": "consent",
|
|
460
|
+
"appDir": "TopoloConsent",
|
|
461
|
+
"name": "topolo-consent",
|
|
462
|
+
"packageName": "topolo-consent",
|
|
463
|
+
"productionUrl": "https://consent.topolo.app",
|
|
464
|
+
"services": [
|
|
465
|
+
"consent"
|
|
466
|
+
],
|
|
467
|
+
"deployTargets": [
|
|
468
|
+
{
|
|
469
|
+
"name": "topolo-consent",
|
|
470
|
+
"kind": "worker",
|
|
471
|
+
"apiUrl": "https://consent.topolo.app",
|
|
472
|
+
"domains": [
|
|
473
|
+
"consent.topolo.app"
|
|
474
|
+
],
|
|
475
|
+
"routes": [
|
|
476
|
+
"consent.topolo.app"
|
|
477
|
+
],
|
|
478
|
+
"serviceId": "consent"
|
|
479
|
+
}
|
|
480
|
+
]
|
|
481
|
+
},
|
|
452
482
|
"crm": {
|
|
453
483
|
"id": "crm",
|
|
454
484
|
"appDir": "TopoloCRM",
|
|
@@ -529,7 +559,9 @@ var APPLICATIONS = {
|
|
|
529
559
|
"name": "topolo-developers",
|
|
530
560
|
"packageName": "topolo-developers",
|
|
531
561
|
"productionUrl": "https://developers.topolo.app",
|
|
532
|
-
"services": [
|
|
562
|
+
"services": [
|
|
563
|
+
"developers"
|
|
564
|
+
],
|
|
533
565
|
"deployTargets": [
|
|
534
566
|
{
|
|
535
567
|
"name": "topolo-catalog-cron",
|
|
@@ -541,13 +573,13 @@ var APPLICATIONS = {
|
|
|
541
573
|
},
|
|
542
574
|
{
|
|
543
575
|
"name": "topolo-developers",
|
|
544
|
-
"kind": "
|
|
545
|
-
"apiUrl":
|
|
576
|
+
"kind": "worker",
|
|
577
|
+
"apiUrl": "https://developers.topolo.app",
|
|
546
578
|
"domains": [
|
|
547
579
|
"developers.topolo.app"
|
|
548
580
|
],
|
|
549
581
|
"routes": [],
|
|
550
|
-
"serviceId":
|
|
582
|
+
"serviceId": "developers"
|
|
551
583
|
}
|
|
552
584
|
]
|
|
553
585
|
},
|
|
@@ -711,6 +743,30 @@ var APPLICATIONS = {
|
|
|
711
743
|
}
|
|
712
744
|
]
|
|
713
745
|
},
|
|
746
|
+
"home": {
|
|
747
|
+
"id": "home",
|
|
748
|
+
"appDir": "TopoloHome",
|
|
749
|
+
"name": "topolo-home",
|
|
750
|
+
"packageName": "topolo-home",
|
|
751
|
+
"productionUrl": "https://home.topolo.app",
|
|
752
|
+
"services": [
|
|
753
|
+
"home"
|
|
754
|
+
],
|
|
755
|
+
"deployTargets": [
|
|
756
|
+
{
|
|
757
|
+
"name": "topolo-home",
|
|
758
|
+
"kind": "worker",
|
|
759
|
+
"apiUrl": "https://home.topolo.app",
|
|
760
|
+
"domains": [
|
|
761
|
+
"home.topolo.app"
|
|
762
|
+
],
|
|
763
|
+
"routes": [
|
|
764
|
+
"home.topolo.app"
|
|
765
|
+
],
|
|
766
|
+
"serviceId": "home"
|
|
767
|
+
}
|
|
768
|
+
]
|
|
769
|
+
},
|
|
714
770
|
"insights": {
|
|
715
771
|
"id": "insights",
|
|
716
772
|
"appDir": "TopoloInsights",
|
|
@@ -2167,6 +2223,16 @@ import { mkdir, readFile, writeFile, chmod } from "fs/promises";
|
|
|
2167
2223
|
import { existsSync } from "fs";
|
|
2168
2224
|
import { homedir, platform } from "os";
|
|
2169
2225
|
import { dirname, join } from "path";
|
|
2226
|
+
var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
|
|
2227
|
+
production: {
|
|
2228
|
+
developers: "https://developers.topolo.app"
|
|
2229
|
+
},
|
|
2230
|
+
staging: {
|
|
2231
|
+
auth: "https://auth.stg.topolo.us",
|
|
2232
|
+
developers: "https://developers.stg.topolo.us",
|
|
2233
|
+
seed: "https://seed.stg.topolo.us"
|
|
2234
|
+
}
|
|
2235
|
+
};
|
|
2170
2236
|
function configPath() {
|
|
2171
2237
|
const override = process.env.TOPOLO_CONFIG_PATH;
|
|
2172
2238
|
if (override) return override;
|
|
@@ -2194,31 +2260,113 @@ async function writeConfig(config) {
|
|
|
2194
2260
|
}
|
|
2195
2261
|
}
|
|
2196
2262
|
}
|
|
2197
|
-
|
|
2198
|
-
const
|
|
2263
|
+
function normalizeEnvironment(value) {
|
|
2264
|
+
const normalized = (value ?? "production").trim().toLowerCase();
|
|
2265
|
+
if (normalized === "" || normalized === "production" || normalized === "prod") {
|
|
2266
|
+
return "production";
|
|
2267
|
+
}
|
|
2268
|
+
if (normalized === "staging" || normalized === "stage" || normalized === "stg") {
|
|
2269
|
+
return "staging";
|
|
2270
|
+
}
|
|
2271
|
+
throw new Error(`Unknown Topolo environment "${value}". Expected production or staging.`);
|
|
2272
|
+
}
|
|
2273
|
+
function selectedEnvironment() {
|
|
2274
|
+
return normalizeEnvironment(process.env.TOPOLO_ENV);
|
|
2275
|
+
}
|
|
2276
|
+
function envVarPrefix(environment) {
|
|
2277
|
+
return environment === "production" ? "TOPOLO_PRODUCTION" : "TOPOLO_STAGING";
|
|
2278
|
+
}
|
|
2279
|
+
function environmentLabel(environment = selectedEnvironment()) {
|
|
2280
|
+
return environment === "production" ? "production" : "staging";
|
|
2281
|
+
}
|
|
2282
|
+
function developerConsoleUrl(environment = selectedEnvironment()) {
|
|
2283
|
+
return environment === "production" ? "https://developers.topolo.app" : "https://developers.stg.topolo.us";
|
|
2284
|
+
}
|
|
2285
|
+
function oauthClientRegistrationUrl(environment = selectedEnvironment()) {
|
|
2286
|
+
return `${developerConsoleUrl(environment)}/developers/oauth-clients`;
|
|
2287
|
+
}
|
|
2288
|
+
function apiKeyIssuanceUrl(environment = selectedEnvironment()) {
|
|
2289
|
+
return `${developerConsoleUrl(environment)}/credentials`;
|
|
2290
|
+
}
|
|
2291
|
+
function profileForEnvironment(config, environment = selectedEnvironment()) {
|
|
2292
|
+
const profile = config.profiles?.[environment] ?? {};
|
|
2293
|
+
if (environment === "production") {
|
|
2294
|
+
return {
|
|
2295
|
+
credential: config.credential,
|
|
2296
|
+
oauthClientId: config.oauthClientId,
|
|
2297
|
+
...profile,
|
|
2298
|
+
serviceUrls: {
|
|
2299
|
+
...config.serviceUrls ?? {},
|
|
2300
|
+
...profile.serviceUrls ?? {}
|
|
2301
|
+
},
|
|
2302
|
+
agentName: profile.agentName ?? config.agentName
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
return profile;
|
|
2306
|
+
}
|
|
2307
|
+
function writeProfileConfig(config, environment, profile) {
|
|
2308
|
+
const profiles = { ...config.profiles ?? {} };
|
|
2309
|
+
profiles[environment] = {
|
|
2310
|
+
...profiles[environment] ?? {},
|
|
2311
|
+
...profile
|
|
2312
|
+
};
|
|
2313
|
+
return { ...config, profiles };
|
|
2314
|
+
}
|
|
2315
|
+
function clearProfileCredential(config, environment) {
|
|
2316
|
+
const next = { ...config };
|
|
2317
|
+
if (environment === "production") {
|
|
2318
|
+
delete next.credential;
|
|
2319
|
+
delete next.oauthClientId;
|
|
2320
|
+
}
|
|
2321
|
+
const profiles = { ...config.profiles ?? {} };
|
|
2322
|
+
const profile = { ...profiles[environment] ?? {} };
|
|
2323
|
+
delete profile.credential;
|
|
2324
|
+
delete profile.oauthClientId;
|
|
2325
|
+
profiles[environment] = profile;
|
|
2326
|
+
return { ...next, profiles };
|
|
2327
|
+
}
|
|
2328
|
+
function resolveOAuthClientId(config, environment = selectedEnvironment()) {
|
|
2329
|
+
const envSpecific = process.env[`${envVarPrefix(environment)}_CLI_CLIENT_ID`];
|
|
2330
|
+
if (envSpecific) return envSpecific;
|
|
2331
|
+
if (process.env.TOPOLO_CLI_CLIENT_ID) return process.env.TOPOLO_CLI_CLIENT_ID;
|
|
2332
|
+
return profileForEnvironment(config, environment).oauthClientId;
|
|
2333
|
+
}
|
|
2334
|
+
function credentialSource(config, environment = selectedEnvironment()) {
|
|
2335
|
+
const prefix = envVarPrefix(environment);
|
|
2336
|
+
if (process.env[`${prefix}_API_KEY`]) return `env:${prefix}_API_KEY`;
|
|
2337
|
+
if (process.env[`${prefix}_ACCESS_TOKEN`]) return `env:${prefix}_ACCESS_TOKEN`;
|
|
2338
|
+
if (process.env.TOPOLO_API_KEY) return "env:TOPOLO_API_KEY";
|
|
2339
|
+
if (process.env.TOPOLO_ACCESS_TOKEN) return "env:TOPOLO_ACCESS_TOKEN";
|
|
2340
|
+
return profileForEnvironment(config, environment).credential ? "config-file" : "none";
|
|
2341
|
+
}
|
|
2342
|
+
async function resolveCredential(environment = selectedEnvironment()) {
|
|
2343
|
+
const prefix = envVarPrefix(environment);
|
|
2344
|
+
const apiKey = process.env[`${prefix}_API_KEY`] ?? process.env.TOPOLO_API_KEY;
|
|
2199
2345
|
if (apiKey) return { kind: "api_key", apiKey };
|
|
2200
|
-
const accessToken = process.env.TOPOLO_ACCESS_TOKEN;
|
|
2346
|
+
const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? process.env.TOPOLO_ACCESS_TOKEN;
|
|
2201
2347
|
if (accessToken) return { kind: "access_token", accessToken };
|
|
2202
2348
|
const config = await readConfig();
|
|
2203
|
-
return config.credential ?? null;
|
|
2349
|
+
return profileForEnvironment(config, environment).credential ?? null;
|
|
2204
2350
|
}
|
|
2205
2351
|
var REFRESH_SKEW_SECONDS = 60;
|
|
2206
|
-
async function refreshCredentialIfNeeded() {
|
|
2207
|
-
|
|
2208
|
-
|
|
2352
|
+
async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
2353
|
+
const prefix = envVarPrefix(environment);
|
|
2354
|
+
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
|
|
2355
|
+
return resolveCredential(environment);
|
|
2209
2356
|
}
|
|
2210
2357
|
const config = await readConfig();
|
|
2211
|
-
const
|
|
2358
|
+
const profile = profileForEnvironment(config, environment);
|
|
2359
|
+
const cred = profile.credential;
|
|
2212
2360
|
if (!cred || cred.kind !== "access_token") return cred ?? null;
|
|
2213
|
-
if (!cred.refreshToken || !cred.expiresAt || !
|
|
2361
|
+
if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
|
|
2214
2362
|
const now = Math.floor(Date.now() / 1e3);
|
|
2215
2363
|
if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
2216
2364
|
try {
|
|
2217
2365
|
const oauth = new TopoloOAuth({
|
|
2218
|
-
serviceUrls: resolveServiceUrls(
|
|
2366
|
+
serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
|
|
2219
2367
|
});
|
|
2220
2368
|
const tokens = await oauth.refreshToken({
|
|
2221
|
-
clientId:
|
|
2369
|
+
clientId: profile.oauthClientId,
|
|
2222
2370
|
refreshToken: cred.refreshToken
|
|
2223
2371
|
});
|
|
2224
2372
|
const refreshed = {
|
|
@@ -2227,7 +2375,9 @@ async function refreshCredentialIfNeeded() {
|
|
|
2227
2375
|
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
|
|
2228
2376
|
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
2229
2377
|
};
|
|
2230
|
-
await writeConfig(
|
|
2378
|
+
await writeConfig(
|
|
2379
|
+
environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed })
|
|
2380
|
+
);
|
|
2231
2381
|
return refreshed;
|
|
2232
2382
|
} catch {
|
|
2233
2383
|
return cred;
|
|
@@ -2236,10 +2386,17 @@ async function refreshCredentialIfNeeded() {
|
|
|
2236
2386
|
function resolveAgentName(configAgentName) {
|
|
2237
2387
|
return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
|
|
2238
2388
|
}
|
|
2239
|
-
function resolveServiceUrls(configUrls) {
|
|
2389
|
+
function resolveServiceUrls(configUrls, environment = selectedEnvironment()) {
|
|
2240
2390
|
const merged = {
|
|
2391
|
+
...ENVIRONMENT_DEFAULT_SERVICE_URLS[environment],
|
|
2241
2392
|
...configUrls ?? {}
|
|
2242
2393
|
};
|
|
2394
|
+
const prefix = `${envVarPrefix(environment)}_SERVICE_URL_`;
|
|
2395
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
2396
|
+
if (k.startsWith(prefix) && v) {
|
|
2397
|
+
merged[k.slice(prefix.length).toLowerCase()] = v;
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2243
2400
|
for (const [k, v] of Object.entries(process.env)) {
|
|
2244
2401
|
if (k.startsWith("TOPOLO_SERVICE_URL_") && v) {
|
|
2245
2402
|
merged[k.slice("TOPOLO_SERVICE_URL_".length).toLowerCase()] = v;
|
|
@@ -2247,6 +2404,28 @@ function resolveServiceUrls(configUrls) {
|
|
|
2247
2404
|
}
|
|
2248
2405
|
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
2249
2406
|
}
|
|
2407
|
+
function resolvedProfileConfig(config, environment = selectedEnvironment()) {
|
|
2408
|
+
const profile = profileForEnvironment(config, environment);
|
|
2409
|
+
return {
|
|
2410
|
+
credential: profile.credential,
|
|
2411
|
+
oauthClientId: profile.oauthClientId,
|
|
2412
|
+
agentName: profile.agentName,
|
|
2413
|
+
serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2416
|
+
function assertServiceUrlForEnvironment(service, serviceUrl, environment = selectedEnvironment()) {
|
|
2417
|
+
if (environment !== "staging") return;
|
|
2418
|
+
if (!serviceUrl) {
|
|
2419
|
+
throw new Error(`No staging URL is configured for Topolo service "${service}".`);
|
|
2420
|
+
}
|
|
2421
|
+
const url = serviceUrl.toLowerCase();
|
|
2422
|
+
if (url.includes(".stg.topolo.us") || url.includes("localhost") || url.includes("127.0.0.1") || url.includes("topolo-staging")) {
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
throw new Error(
|
|
2426
|
+
`Refusing to call ${service} in staging because its effective URL is not a staging endpoint: ${serviceUrl}`
|
|
2427
|
+
);
|
|
2428
|
+
}
|
|
2250
2429
|
|
|
2251
2430
|
// src/output.ts
|
|
2252
2431
|
function resolveOutputMode(override) {
|
|
@@ -2293,38 +2472,38 @@ function normalizeError(err) {
|
|
|
2293
2472
|
}
|
|
2294
2473
|
|
|
2295
2474
|
// src/commands/auth.ts
|
|
2296
|
-
var API_KEY_ISSUANCE_URL = "https://developers.topolo.app/credentials";
|
|
2297
|
-
var OAUTH_CLIENT_REGISTRATION_URL = "https://developers.topolo.app/developers/oauth-clients";
|
|
2298
2475
|
function registerAuthCommands(program2) {
|
|
2299
2476
|
const auth = program2.command("auth").description("Manage Topolo credentials");
|
|
2300
2477
|
auth.command("login").description("Authenticate with Topolo via OAuth device flow (default) or store an API key").option("--api-key <key>", "Store an API key directly, skipping the OAuth flow").option("--client-id <id>", "OAuth client_id (or set TOPOLO_CLI_CLIENT_ID)").option("--scope <scope>", "Scopes to request (space-separated)").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(
|
|
2301
2478
|
async (opts) => {
|
|
2302
2479
|
const mode = resolveOutputMode(opts.json);
|
|
2480
|
+
const environment = selectedEnvironment();
|
|
2303
2481
|
try {
|
|
2304
2482
|
if (opts.apiKey) {
|
|
2305
|
-
await storeApiKey(opts.apiKey.trim(), mode);
|
|
2483
|
+
await storeApiKey(opts.apiKey.trim(), environment, mode);
|
|
2306
2484
|
return;
|
|
2307
2485
|
}
|
|
2308
|
-
const
|
|
2486
|
+
const config = await readConfig();
|
|
2487
|
+
const clientId = opts.clientId ?? resolveOAuthClientId(config, environment);
|
|
2309
2488
|
if (!clientId) {
|
|
2310
2489
|
if (!input.isTTY) {
|
|
2311
2490
|
throw new Error(
|
|
2312
|
-
`No OAuth client_id configured and stdin is not a TTY. Either register an OAuth client at ${
|
|
2491
|
+
`No OAuth client_id configured and stdin is not a TTY. Either register an OAuth client at ${oauthClientRegistrationUrl(environment)} and export TOPOLO_${environment === "staging" ? "STAGING" : "PRODUCTION"}_CLI_CLIENT_ID, or pass --api-key <key> (mint one at ${apiKeyIssuanceUrl(environment)}).`
|
|
2313
2492
|
);
|
|
2314
2493
|
}
|
|
2315
2494
|
process.stderr.write(
|
|
2316
|
-
`No OAuth client_id configured. Falling back to API key.
|
|
2317
|
-
Visit ${
|
|
2495
|
+
`No OAuth client_id configured for ${environmentLabel(environment)}. Falling back to API key.
|
|
2496
|
+
Visit ${apiKeyIssuanceUrl(environment)} to mint one, or configure an OAuth client at ${oauthClientRegistrationUrl(environment)} and set the environment-specific CLI client id.
|
|
2318
2497
|
`
|
|
2319
2498
|
);
|
|
2320
2499
|
const rl = createInterface({ input, output: process.stderr });
|
|
2321
2500
|
const apiKey = (await rl.question("Paste API key: ")).trim();
|
|
2322
2501
|
rl.close();
|
|
2323
2502
|
if (!apiKey) throw new Error("API key is required");
|
|
2324
|
-
await storeApiKey(apiKey, mode);
|
|
2503
|
+
await storeApiKey(apiKey, environment, mode);
|
|
2325
2504
|
return;
|
|
2326
2505
|
}
|
|
2327
|
-
await runDeviceFlow({ clientId, scope: opts.scope, mode });
|
|
2506
|
+
await runDeviceFlow({ clientId, environment, scope: opts.scope, mode });
|
|
2328
2507
|
} catch (err) {
|
|
2329
2508
|
fail(err, mode);
|
|
2330
2509
|
}
|
|
@@ -2332,40 +2511,65 @@ Visit ${API_KEY_ISSUANCE_URL} to mint one, or configure an OAuth client at ${OAU
|
|
|
2332
2511
|
);
|
|
2333
2512
|
auth.command("logout").description("Remove stored credentials from this machine").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
2334
2513
|
const mode = resolveOutputMode(opts.json);
|
|
2514
|
+
const environment = selectedEnvironment();
|
|
2335
2515
|
try {
|
|
2336
2516
|
const config = await readConfig();
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2517
|
+
await writeConfig(clearProfileCredential(config, environment));
|
|
2518
|
+
emit(
|
|
2519
|
+
{ ok: true, environment },
|
|
2520
|
+
mode,
|
|
2521
|
+
() => `Logged out of Topolo ${environmentLabel(environment)}.`
|
|
2522
|
+
);
|
|
2340
2523
|
} catch (err) {
|
|
2341
2524
|
fail(err, mode);
|
|
2342
2525
|
}
|
|
2343
2526
|
});
|
|
2344
2527
|
auth.command("status").description("Show which credential source is in use (env vs config file)").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
2345
2528
|
const mode = resolveOutputMode(opts.json);
|
|
2529
|
+
const environment = selectedEnvironment();
|
|
2346
2530
|
try {
|
|
2347
|
-
const
|
|
2348
|
-
|
|
2531
|
+
const config = await readConfig();
|
|
2532
|
+
const source = credentialSource(config, environment);
|
|
2533
|
+
const profile = resolvedProfileConfig(config, environment);
|
|
2534
|
+
emit(
|
|
2535
|
+
{
|
|
2536
|
+
environment,
|
|
2537
|
+
source,
|
|
2538
|
+
authUrl: profile.serviceUrls?.auth ?? null,
|
|
2539
|
+
developersUrl: profile.serviceUrls?.developers ?? null
|
|
2540
|
+
},
|
|
2541
|
+
mode,
|
|
2542
|
+
() => [
|
|
2543
|
+
`Environment: ${environmentLabel(environment)}`,
|
|
2544
|
+
`Credential source: ${source}`,
|
|
2545
|
+
`Auth URL: ${profile.serviceUrls?.auth ?? "(sdk default)"}`,
|
|
2546
|
+
`Developers URL: ${profile.serviceUrls?.developers ?? "(sdk default)"}`
|
|
2547
|
+
].join("\n")
|
|
2548
|
+
);
|
|
2349
2549
|
} catch (err) {
|
|
2350
2550
|
fail(err, mode);
|
|
2351
2551
|
}
|
|
2352
2552
|
});
|
|
2353
2553
|
}
|
|
2354
|
-
async function storeApiKey(apiKey, mode) {
|
|
2554
|
+
async function storeApiKey(apiKey, environment, mode) {
|
|
2355
2555
|
if (!apiKey) throw new Error("API key is required");
|
|
2356
2556
|
const config = await readConfig();
|
|
2357
|
-
|
|
2358
|
-
|
|
2557
|
+
await writeConfig(
|
|
2558
|
+
writeProfileConfig(config, environment, {
|
|
2559
|
+
credential: { kind: "api_key", apiKey }
|
|
2560
|
+
})
|
|
2561
|
+
);
|
|
2359
2562
|
emit(
|
|
2360
|
-
{ ok: true, credential: { kind: "api_key" } },
|
|
2563
|
+
{ ok: true, environment, credential: { kind: "api_key" } },
|
|
2361
2564
|
mode,
|
|
2362
|
-
() =>
|
|
2565
|
+
() => `Stored API key for Topolo ${environmentLabel(environment)}. Run \`topolo whoami\` to verify.`
|
|
2363
2566
|
);
|
|
2364
2567
|
}
|
|
2365
2568
|
async function runDeviceFlow(options) {
|
|
2366
2569
|
const config = await readConfig();
|
|
2570
|
+
const resolved = resolvedProfileConfig(config, options.environment);
|
|
2367
2571
|
const oauth = new TopoloOAuth({
|
|
2368
|
-
serviceUrls:
|
|
2572
|
+
serviceUrls: resolved.serviceUrls
|
|
2369
2573
|
});
|
|
2370
2574
|
const start = await oauth.requestDeviceCode({
|
|
2371
2575
|
clientId: options.clientId,
|
|
@@ -2377,7 +2581,7 @@ async function runDeviceFlow(options) {
|
|
|
2377
2581
|
if (options.mode === "pretty") {
|
|
2378
2582
|
process.stderr.write(
|
|
2379
2583
|
`
|
|
2380
|
-
Sign in to Topolo
|
|
2584
|
+
Sign in to Topolo ${environmentLabel(options.environment)}
|
|
2381
2585
|
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2382
2586
|
|
|
2383
2587
|
` + (opened ? ` Opening your browser to approve...
|
|
@@ -2405,17 +2609,21 @@ async function runDeviceFlow(options) {
|
|
|
2405
2609
|
spinner?.stop();
|
|
2406
2610
|
}
|
|
2407
2611
|
const updated = await readConfig();
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2612
|
+
await writeConfig(
|
|
2613
|
+
writeProfileConfig(updated, options.environment, {
|
|
2614
|
+
credential: {
|
|
2615
|
+
kind: "access_token",
|
|
2616
|
+
accessToken: token.access_token,
|
|
2617
|
+
...token.refresh_token ? { refreshToken: token.refresh_token } : {},
|
|
2618
|
+
expiresAt: Math.floor(Date.now() / 1e3) + token.expires_in
|
|
2619
|
+
},
|
|
2620
|
+
oauthClientId: options.clientId
|
|
2621
|
+
})
|
|
2622
|
+
);
|
|
2416
2623
|
emit(
|
|
2417
2624
|
{
|
|
2418
2625
|
ok: true,
|
|
2626
|
+
environment: options.environment,
|
|
2419
2627
|
credential: {
|
|
2420
2628
|
kind: "access_token",
|
|
2421
2629
|
expiresIn: token.expires_in,
|
|
@@ -2491,22 +2699,23 @@ function sleep(ms) {
|
|
|
2491
2699
|
}
|
|
2492
2700
|
|
|
2493
2701
|
// src/version.ts
|
|
2494
|
-
var CLI_VERSION = "0.1.
|
|
2702
|
+
var CLI_VERSION = "0.1.17";
|
|
2495
2703
|
|
|
2496
2704
|
// src/client.ts
|
|
2497
2705
|
async function buildClient(options) {
|
|
2498
|
-
const
|
|
2706
|
+
const environment = selectedEnvironment();
|
|
2707
|
+
const credential = await refreshCredentialIfNeeded(environment);
|
|
2499
2708
|
if (!credential) return null;
|
|
2500
2709
|
const config = await readConfig();
|
|
2501
|
-
const
|
|
2710
|
+
const profile = resolvedProfileConfig(config, environment);
|
|
2502
2711
|
return createTopolo({
|
|
2503
2712
|
credential,
|
|
2504
2713
|
agent: {
|
|
2505
2714
|
clientName: "topolo-cli",
|
|
2506
2715
|
clientVersion: CLI_VERSION,
|
|
2507
|
-
agentName: resolveAgentName(
|
|
2716
|
+
agentName: resolveAgentName(profile.agentName)
|
|
2508
2717
|
},
|
|
2509
|
-
serviceUrls,
|
|
2718
|
+
serviceUrls: profile.serviceUrls,
|
|
2510
2719
|
requireConfirmForWrites: options?.requireConfirmForWrites,
|
|
2511
2720
|
...isDebugEnabled() ? { debug: stderrDebugLogger } : {}
|
|
2512
2721
|
});
|
|
@@ -2543,6 +2752,9 @@ function registerWhoamiCommand(program2) {
|
|
|
2543
2752
|
program2.command("whoami").description("Show the user, organization, and scopes attached to the current credential").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
2544
2753
|
const mode = resolveOutputMode(opts.json);
|
|
2545
2754
|
try {
|
|
2755
|
+
const environment = selectedEnvironment();
|
|
2756
|
+
const profile = resolvedProfileConfig(await readConfig(), environment);
|
|
2757
|
+
assertServiceUrlForEnvironment("auth", profile.serviceUrls?.auth, environment);
|
|
2546
2758
|
const topolo = await buildClient();
|
|
2547
2759
|
if (!topolo) {
|
|
2548
2760
|
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
@@ -2573,6 +2785,7 @@ function registerCrmCommands(program2) {
|
|
|
2573
2785
|
crm.command("contacts:list").description("List contacts in the authenticated organization").option("-q, --query <text>", "Full-text search").option("-p, --page <n>", "Page number (1-indexed)", (v) => parseInt(v, 10), 1).option("-s, --page-size <n>", "Page size", (v) => parseInt(v, 10), 25).option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (opts) => {
|
|
2574
2786
|
const mode = resolveOutputMode(opts.json);
|
|
2575
2787
|
try {
|
|
2788
|
+
await assertCrmRoutable();
|
|
2576
2789
|
const topolo = await buildClient();
|
|
2577
2790
|
if (!topolo) {
|
|
2578
2791
|
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
@@ -2590,6 +2803,7 @@ function registerCrmCommands(program2) {
|
|
|
2590
2803
|
crm.command("contacts:get <id>").description("Fetch a single contact by id").option("--json", "Output JSON").option("--no-json", "Output human-readable text").action(async (id, opts) => {
|
|
2591
2804
|
const mode = resolveOutputMode(opts.json);
|
|
2592
2805
|
try {
|
|
2806
|
+
await assertCrmRoutable();
|
|
2593
2807
|
const topolo = await buildClient();
|
|
2594
2808
|
if (!topolo) {
|
|
2595
2809
|
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
@@ -2602,6 +2816,15 @@ function registerCrmCommands(program2) {
|
|
|
2602
2816
|
}
|
|
2603
2817
|
});
|
|
2604
2818
|
}
|
|
2819
|
+
async function assertCrmRoutable() {
|
|
2820
|
+
const environment = selectedEnvironment();
|
|
2821
|
+
const profile = resolvedProfileConfig(await readConfig(), environment);
|
|
2822
|
+
assertServiceUrlForEnvironment(
|
|
2823
|
+
"crm",
|
|
2824
|
+
resolveServiceUrl("crm", profile.serviceUrls),
|
|
2825
|
+
environment
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2605
2828
|
function formatContactsTable(rows, total, page) {
|
|
2606
2829
|
if (rows.length === 0) return "No contacts found.";
|
|
2607
2830
|
const lines = [];
|
|
@@ -2660,6 +2883,14 @@ function registerApiCommand(program2) {
|
|
|
2660
2883
|
if (WRITE_METHODS2.has(httpMethod) && !opts.confirm) {
|
|
2661
2884
|
throw new Error(`Mutating ${httpMethod} requires --confirm.`);
|
|
2662
2885
|
}
|
|
2886
|
+
const environment = selectedEnvironment();
|
|
2887
|
+
const config = await readConfig();
|
|
2888
|
+
const profile = resolvedProfileConfig(config, environment);
|
|
2889
|
+
assertServiceUrlForEnvironment(
|
|
2890
|
+
service,
|
|
2891
|
+
resolveServiceUrl(service, profile.serviceUrls),
|
|
2892
|
+
environment
|
|
2893
|
+
);
|
|
2663
2894
|
let body = void 0;
|
|
2664
2895
|
if (opts.data) {
|
|
2665
2896
|
try {
|
|
@@ -3641,7 +3872,9 @@ import { constants } from "fs";
|
|
|
3641
3872
|
import { access, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
3642
3873
|
import { dirname as dirname2, relative, resolve } from "path";
|
|
3643
3874
|
var CLOUDFLARE_ACCOUNT_ID = "49ef1ba682ad8cfd720c86699ae17521";
|
|
3644
|
-
var
|
|
3875
|
+
var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
|
|
3876
|
+
var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
|
|
3877
|
+
var CURRENT_VERIFICATION_DATE = "2026-05-06";
|
|
3645
3878
|
function applicationScaffoldPayload(application, options = {}) {
|
|
3646
3879
|
const target = normalizeTarget(options.target);
|
|
3647
3880
|
const portfolio = target === "platform" ? normalizePortfolio(options.portfolio) : null;
|
|
@@ -4148,9 +4381,13 @@ function packageJson(application, target, options) {
|
|
|
4148
4381
|
if (target === "platform") {
|
|
4149
4382
|
dependencies["@topolo-io/auth-middleware"] = topoloPackageVersion("@topolo-io/auth-middleware", target);
|
|
4150
4383
|
dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
|
|
4384
|
+
scripts.deploy = `node -e 'console.error("\\nX Bare deploy is disabled. Use deploy:staging or deploy:production.\\n"); process.exit(1)'`;
|
|
4385
|
+
scripts["deploy:staging"] = "DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build && wrangler deploy --env staging";
|
|
4386
|
+
scripts["deploy:production"] = "DEPLOY_ENVIRONMENT=production npm run build && wrangler deploy";
|
|
4387
|
+
} else {
|
|
4388
|
+
scripts.deploy = "npm run build && wrangler deploy";
|
|
4151
4389
|
}
|
|
4152
4390
|
devDependencies.wrangler = "^4.40.2";
|
|
4153
|
-
scripts.deploy = "npm run build && wrangler deploy";
|
|
4154
4391
|
}
|
|
4155
4392
|
return `${json({
|
|
4156
4393
|
name: application.packageName,
|
|
@@ -4485,8 +4722,13 @@ function appTsx(application, target, options) {
|
|
|
4485
4722
|
function platformAppTsx(application, options) {
|
|
4486
4723
|
const appName = toTitle(application.name);
|
|
4487
4724
|
const serviceId = application.serviceId ?? application.id;
|
|
4488
|
-
const
|
|
4489
|
-
const
|
|
4725
|
+
const isPersonal = options.audience === "personal";
|
|
4726
|
+
const tagline = isPersonal ? "Topolo personal app" : "Topolo workspace";
|
|
4727
|
+
const workspaceTitle = isPersonal ? "Personal app" : "Workspace";
|
|
4728
|
+
const shellLabel = isPersonal ? "personal app shell" : "workspace shell";
|
|
4729
|
+
const signedSurfaceLabel = isPersonal ? "personal app" : "workspace";
|
|
4730
|
+
const baselineHeading = isPersonal ? "Shared personal baseline" : "Shared workspace baseline";
|
|
4731
|
+
const baselineDescription = isPersonal ? "This scaffold starts on the shared landing and login surfaces, completes sign-in on the shared callback path, and renders the signed personal app inside the shared Topolo shell." : "This scaffold starts on the shared landing and login surfaces, completes sign-in on the shared callback path, and renders the signed workspace inside the shared Topolo shell.";
|
|
4490
4732
|
return [
|
|
4491
4733
|
"import { Component, useEffect, useMemo, useState, type ErrorInfo, type ReactNode } from 'react';",
|
|
4492
4734
|
"import { createBrowserAuthModule, type AuthUser } from '@topolo-io/auth-client';",
|
|
@@ -4512,17 +4754,17 @@ function platformAppTsx(application, options) {
|
|
|
4512
4754
|
" app_name: APP_NAME,",
|
|
4513
4755
|
` app_tagline: '${escapeSingleQuoted(tagline)}',`,
|
|
4514
4756
|
" hero_title: `Open ${APP_NAME} in Topolo`,",
|
|
4515
|
-
|
|
4757
|
+
` hero_subtitle: 'Shared landing, login, callback, auth, and ${escapeSingleQuoted(shellLabel)} wiring are already included in the generated baseline.',`,
|
|
4516
4758
|
" hero_cta_text: 'Sign in',",
|
|
4517
4759
|
" hero_cta_url: '/login',",
|
|
4518
4760
|
" cta_title: 'Start from the shared platform entry flow',",
|
|
4519
|
-
|
|
4761
|
+
` cta_subtitle: 'Unauthenticated routes stay on the shared Topolo surfaces before users enter the signed ${escapeSingleQuoted(shellLabel)}.',`,
|
|
4520
4762
|
"};",
|
|
4521
4763
|
"const LOGIN_CONFIG = {",
|
|
4522
4764
|
" app_name: APP_NAME,",
|
|
4523
4765
|
` app_tagline: '${escapeSingleQuoted(tagline)}',`,
|
|
4524
4766
|
" hero_title: `Sign in to ${APP_NAME}`,",
|
|
4525
|
-
|
|
4767
|
+
` hero_subtitle: 'Use Topolo Auth to open the shared ${escapeSingleQuoted(shellLabel)} for this application.',`,
|
|
4526
4768
|
" authenticated_home_path: '/',",
|
|
4527
4769
|
"};",
|
|
4528
4770
|
"",
|
|
@@ -4542,6 +4784,7 @@ function platformAppTsx(application, options) {
|
|
|
4542
4784
|
"function toShellUser(user: AuthUser | null): TopoloAppShellUser | null {",
|
|
4543
4785
|
" if (!user) return null;",
|
|
4544
4786
|
" return {",
|
|
4787
|
+
" id: user.id,",
|
|
4545
4788
|
" name: user.name ?? user.email,",
|
|
4546
4789
|
" email: user.email,",
|
|
4547
4790
|
" role: user.role,",
|
|
@@ -4690,7 +4933,7 @@ function platformAppTsx(application, options) {
|
|
|
4690
4933
|
" appId={APP_ID}",
|
|
4691
4934
|
" serviceId={SERVICE_ID}",
|
|
4692
4935
|
" appName={APP_NAME}",
|
|
4693
|
-
|
|
4936
|
+
` subtitle="Opening your ${escapeDoubleQuoted(signedSurfaceLabel)}..."`,
|
|
4694
4937
|
" />",
|
|
4695
4938
|
" );",
|
|
4696
4939
|
" }",
|
|
@@ -4701,6 +4944,9 @@ function platformAppTsx(application, options) {
|
|
|
4701
4944
|
" serviceId={SERVICE_ID}",
|
|
4702
4945
|
" appName={APP_NAME}",
|
|
4703
4946
|
" user={shellUser}",
|
|
4947
|
+
" orgId={user.activeContextType === 'organization' ? user.activeContextId ?? user.orgId ?? null : null}",
|
|
4948
|
+
" activeContextType={user.activeContextType}",
|
|
4949
|
+
" activeContextId={user.activeContextId}",
|
|
4704
4950
|
' activeNavItemId="home"',
|
|
4705
4951
|
` navItems={[{ id: 'home', label: '${escapeSingleQuoted(workspaceTitle)}' }]}`,
|
|
4706
4952
|
' navigationMode="topbar"',
|
|
@@ -4719,9 +4965,9 @@ function platformAppTsx(application, options) {
|
|
|
4719
4965
|
" >",
|
|
4720
4966
|
' <div className="topolo-starter-workspace">',
|
|
4721
4967
|
' <section className="topolo-starter-card">',
|
|
4722
|
-
|
|
4968
|
+
` <h2>${baselineHeading}</h2>`,
|
|
4723
4969
|
" <p>",
|
|
4724
|
-
|
|
4970
|
+
` ${baselineDescription}`,
|
|
4725
4971
|
" </p>",
|
|
4726
4972
|
" </section>",
|
|
4727
4973
|
" </div>",
|
|
@@ -4913,7 +5159,7 @@ function wranglerToml(application, target) {
|
|
|
4913
5159
|
const lines = [
|
|
4914
5160
|
`name = "${workerName}"`,
|
|
4915
5161
|
'main = "src/worker.ts"',
|
|
4916
|
-
`compatibility_date = "${
|
|
5162
|
+
`compatibility_date = "${CLOUDFLARE_COMPATIBILITY_DATE}"`,
|
|
4917
5163
|
'compatibility_flags = ["nodejs_compat"]'
|
|
4918
5164
|
];
|
|
4919
5165
|
if (target === "platform") {
|
|
@@ -4948,15 +5194,51 @@ function wranglerToml(application, target) {
|
|
|
4948
5194
|
`SERVICE_ID = "${application.serviceId ?? application.id}"`,
|
|
4949
5195
|
""
|
|
4950
5196
|
);
|
|
5197
|
+
if (target === "platform") {
|
|
5198
|
+
lines.push(
|
|
5199
|
+
"# Staging deploys use the separate Topolo Staging Cloudflare account.",
|
|
5200
|
+
"[env.staging]",
|
|
5201
|
+
`name = "${workerName}-staging"`,
|
|
5202
|
+
`account_id = "${STAGING_CLOUDFLARE_ACCOUNT_ID}"`,
|
|
5203
|
+
"workers_dev = true",
|
|
5204
|
+
"routes = [",
|
|
5205
|
+
` { pattern = "${slug}.stg.topolo.us", custom_domain = true }`,
|
|
5206
|
+
"]",
|
|
5207
|
+
"",
|
|
5208
|
+
"[env.staging.vars]",
|
|
5209
|
+
'AUTH_BASE_URL = "https://auth.stg.topolo.us"',
|
|
5210
|
+
`SERVICE_ID = "${application.serviceId ?? application.id}"`,
|
|
5211
|
+
""
|
|
5212
|
+
);
|
|
5213
|
+
}
|
|
4951
5214
|
return lines.join("\n");
|
|
4952
5215
|
}
|
|
4953
|
-
function workerTs(application, target,
|
|
5216
|
+
function workerTs(application, target, options) {
|
|
4954
5217
|
if (target === "developer") return developerWorkerTs(application);
|
|
4955
|
-
return platformWorkerTs(application);
|
|
5218
|
+
return platformWorkerTs(application, options);
|
|
4956
5219
|
}
|
|
4957
|
-
function platformWorkerTs(application) {
|
|
5220
|
+
function platformWorkerTs(application, options) {
|
|
4958
5221
|
const serviceId = application.serviceId ?? application.id;
|
|
4959
5222
|
const serviceName = toTitle(application.name);
|
|
5223
|
+
const openActionLabel = options.audience === "personal" ? "Open app" : "Open workspace";
|
|
5224
|
+
const bootstrapBody = options.tenancy === "household" ? [
|
|
5225
|
+
" const userContext = auth.user && typeof auth.user === 'object' ? (auth.user as unknown as Record<string, unknown>) : {};",
|
|
5226
|
+
" return json({",
|
|
5227
|
+
" user: auth.user,",
|
|
5228
|
+
" activeContextType: userContext.activeContextType ?? null,",
|
|
5229
|
+
" activeContextId: userContext.activeContextId ?? null,",
|
|
5230
|
+
" activeContext: userContext.activeContext ?? null,",
|
|
5231
|
+
" selectedHouseholdId: userContext.selectedHouseholdId ?? null,",
|
|
5232
|
+
" selectedHousehold: userContext.selectedHousehold ?? null,",
|
|
5233
|
+
" household: userContext.selectedHousehold ?? null,",
|
|
5234
|
+
" householdMemberships: userContext.householdMemberships ?? [],",
|
|
5235
|
+
" }, { status: 200 }, request);"
|
|
5236
|
+
] : [
|
|
5237
|
+
" return json({",
|
|
5238
|
+
" user: auth.user,",
|
|
5239
|
+
" organization: auth.organization,",
|
|
5240
|
+
" }, { status: 200 }, request);"
|
|
5241
|
+
];
|
|
4960
5242
|
return [
|
|
4961
5243
|
"import { createTopoloWidgetResponse } from '@topolo/sdk';",
|
|
4962
5244
|
"import { verifyRequest } from '@topolo-io/auth-middleware';",
|
|
@@ -4995,7 +5277,7 @@ function platformWorkerTs(application) {
|
|
|
4995
5277
|
" {",
|
|
4996
5278
|
" type: 'quick-actions',",
|
|
4997
5279
|
" actions: [",
|
|
4998
|
-
|
|
5280
|
+
` { id: 'open-workspace', label: '${escapeSingleQuoted(openActionLabel)}', link: '/', primary: true },`,
|
|
4999
5281
|
" { id: 'view-health', label: 'View health', link: '/api/health' },",
|
|
5000
5282
|
" ],",
|
|
5001
5283
|
" },",
|
|
@@ -5004,10 +5286,7 @@ function platformWorkerTs(application) {
|
|
|
5004
5286
|
" }",
|
|
5005
5287
|
"",
|
|
5006
5288
|
" if (url.pathname === '/api/bootstrap') {",
|
|
5007
|
-
|
|
5008
|
-
" user: auth.user,",
|
|
5009
|
-
" organization: auth.organization,",
|
|
5010
|
-
" }, { status: 200 }, request);",
|
|
5289
|
+
...bootstrapBody,
|
|
5011
5290
|
" }",
|
|
5012
5291
|
"",
|
|
5013
5292
|
" return json({ error: 'not_found' }, { status: 404 }, request);",
|
|
@@ -5102,6 +5381,7 @@ function developerWorkerTs(application) {
|
|
|
5102
5381
|
function cloudcontrolJson(application, options) {
|
|
5103
5382
|
const slug = toSlug(application.id);
|
|
5104
5383
|
const host = application.productionUrl ? new URL(application.productionUrl).host : `${slug}.topolo.app`;
|
|
5384
|
+
const stagingHost = `${slug}.stg.topolo.us`;
|
|
5105
5385
|
const deployTargets = options.api ? [
|
|
5106
5386
|
{
|
|
5107
5387
|
kind: "worker",
|
|
@@ -5117,6 +5397,21 @@ function cloudcontrolJson(application, options) {
|
|
|
5117
5397
|
routes: [host],
|
|
5118
5398
|
api_url: application.productionUrl,
|
|
5119
5399
|
service_id: application.serviceId
|
|
5400
|
+
},
|
|
5401
|
+
{
|
|
5402
|
+
kind: "worker",
|
|
5403
|
+
environment: "staging",
|
|
5404
|
+
target_name: `topolo-${slug}-staging`,
|
|
5405
|
+
cloudflare_account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
5406
|
+
config_path: "wrangler.toml",
|
|
5407
|
+
build_command: "VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build",
|
|
5408
|
+
output_dir: "dist",
|
|
5409
|
+
production_branch: "staging",
|
|
5410
|
+
healthcheck_path: "/api/health",
|
|
5411
|
+
domains: [stagingHost],
|
|
5412
|
+
routes: [stagingHost],
|
|
5413
|
+
api_url: `https://${stagingHost}`,
|
|
5414
|
+
service_id: application.serviceId
|
|
5120
5415
|
}
|
|
5121
5416
|
] : [
|
|
5122
5417
|
{
|
|
@@ -5152,6 +5447,12 @@ function cloudcontrolJson(application, options) {
|
|
|
5152
5447
|
url: application.productionUrl,
|
|
5153
5448
|
branch: "main",
|
|
5154
5449
|
status: "unknown"
|
|
5450
|
+
},
|
|
5451
|
+
{
|
|
5452
|
+
name: "staging",
|
|
5453
|
+
url: `https://${stagingHost}`,
|
|
5454
|
+
branch: "staging",
|
|
5455
|
+
status: "unknown"
|
|
5155
5456
|
}
|
|
5156
5457
|
] : [],
|
|
5157
5458
|
deploy_targets: deployTargets,
|
|
@@ -5175,6 +5476,9 @@ function systemJson(application, options) {
|
|
|
5175
5476
|
const slug = toSlug(application.id);
|
|
5176
5477
|
const suiteLabel = options.audience === "personal" ? "personal" : "platform";
|
|
5177
5478
|
const summaryLabel = options.audience === "personal" ? "first-party Topolo personal application" : "first-party Topolo platform application";
|
|
5479
|
+
const apiNotes = options.api ? options.tenancy === "household" ? "Protected Worker routes validate Topolo Auth credentials and derive active personal context plus selected household context from the credential. The browser baseline starts on shared landing and login routes, completes auth on /auth/callback, and dedupes React when linked Topolo packages are installed locally." : "Protected Worker routes validate Topolo Auth credentials and derive organization context from the credential. The browser baseline starts on shared landing and login routes, completes auth on /auth/callback, and dedupes React when linked Topolo packages are installed locally." : "Browser-only application surface using shared Topolo Auth landing/login/callback routes, shared shell packages, and React runtime dedupe.";
|
|
5480
|
+
const sensitiveData = options.tenancy === "household" ? ["identity", "personal_data", "household_data"] : ["identity", "org_data"];
|
|
5481
|
+
const privacySensitiveData = options.tenancy === "household" ? ["identity", "personal", "household"] : ["identity", "organization"];
|
|
5178
5482
|
return `${json({
|
|
5179
5483
|
id: `topolo-${slug}`,
|
|
5180
5484
|
name: toTitle(application.name),
|
|
@@ -5190,7 +5494,7 @@ function systemJson(application, options) {
|
|
|
5190
5494
|
api_contract: {
|
|
5191
5495
|
type: options.api ? "curated" : "none",
|
|
5192
5496
|
source: options.api ? `PlatformApplications/${application.appDir}/src/worker.ts` : `PlatformApplications/${application.appDir}/src/App.tsx`,
|
|
5193
|
-
notes:
|
|
5497
|
+
notes: apiNotes
|
|
5194
5498
|
},
|
|
5195
5499
|
primary_hosts: application.productionUrl ? [application.productionUrl] : [],
|
|
5196
5500
|
doc_paths: [`internal/apps/${slug}`, `public/applications/${slug}`],
|
|
@@ -5199,12 +5503,26 @@ function systemJson(application, options) {
|
|
|
5199
5503
|
auth_boundary: "Topolo Auth through @topolo-io/auth-client for browser sessions and @topolo-io/auth-middleware for Worker requests.",
|
|
5200
5504
|
tenant_isolation: options.tenancy === "household" ? "household_scoped" : "organization_scoped",
|
|
5201
5505
|
external_inputs: ["browser", ...options.api ? ["api"] : []],
|
|
5202
|
-
sensitive_data:
|
|
5506
|
+
sensitive_data: sensitiveData,
|
|
5203
5507
|
security_review_status: "not_started",
|
|
5204
5508
|
last_security_review: CURRENT_VERIFICATION_DATE,
|
|
5205
5509
|
pentest_status: "not_started",
|
|
5206
5510
|
evidence_doc: `internal/apps/${slug}`
|
|
5207
5511
|
},
|
|
5512
|
+
data_privacy: {
|
|
5513
|
+
classification: "confidential",
|
|
5514
|
+
sensitive_data_classes: privacySensitiveData,
|
|
5515
|
+
storage_locations: ["source_control"],
|
|
5516
|
+
encryption_at_rest: "platform_managed",
|
|
5517
|
+
encryption_in_transit: "https_only",
|
|
5518
|
+
key_management: "cloudflare_managed",
|
|
5519
|
+
retention_policy: "needs_policy",
|
|
5520
|
+
deletion_export_status: "planned",
|
|
5521
|
+
logging_redaction_status: "partial",
|
|
5522
|
+
privacy_review_status: "not_started",
|
|
5523
|
+
enterprise_ready: false,
|
|
5524
|
+
evidence_doc: `internal/apps/${slug}`
|
|
5525
|
+
},
|
|
5208
5526
|
dependencies: options.audience === "personal" ? ["topolo-auth"] : ["topolo-auth", "topolo-platform"],
|
|
5209
5527
|
tags: ["application", "cloudflare", "typescript", suiteLabel]
|
|
5210
5528
|
})}
|
|
@@ -5215,6 +5533,8 @@ function internalDocs(application, options) {
|
|
|
5215
5533
|
const title = toTitle(application.name);
|
|
5216
5534
|
const suiteLabel = options.audience === "personal" ? "personal" : "platform";
|
|
5217
5535
|
const tenantLabel = options.tenancy === "household" ? "household" : "organization";
|
|
5536
|
+
const apiContextLabel = options.tenancy === "household" ? "active personal context and selected household context" : "organization context";
|
|
5537
|
+
const boundaryFailureText = options.tenancy === "household" ? "Domain data is added without a household-scoped backend boundary." : "Domain data is added without an organization-scoped backend boundary.";
|
|
5218
5538
|
return [
|
|
5219
5539
|
"---",
|
|
5220
5540
|
`title: "${escapeFrontmatter(title)} Handbook"`,
|
|
@@ -5257,11 +5577,11 @@ function internalDocs(application, options) {
|
|
|
5257
5577
|
"",
|
|
5258
5578
|
"## API Reference",
|
|
5259
5579
|
"",
|
|
5260
|
-
options.api ?
|
|
5580
|
+
options.api ? `The generated API baseline exposes \`/api/health\`, \`/api/widget\`, and \`/api/bootstrap\`. \`/api/widget\` returns the SDK widget response contract for TopoloOne, while \`/api/bootstrap\` requires Topolo Auth validation and returns the verified user plus ${apiContextLabel}.` : "This generated baseline is browser-only and does not expose a callable API service.",
|
|
5261
5581
|
"",
|
|
5262
5582
|
"## Auth and Permissions",
|
|
5263
5583
|
"",
|
|
5264
|
-
options.api ? `Browser sessions and Worker requests must be validated through Topolo Auth under service id \`${application.serviceId}\`. Worker routes derive
|
|
5584
|
+
options.api ? `Browser sessions and Worker requests must be validated through Topolo Auth under service id \`${application.serviceId}\`. Worker routes derive ${apiContextLabel} from the verified credential.` : "Browser sessions must be established through Topolo Auth and rendered inside the shared Topolo shell.",
|
|
5265
5585
|
"",
|
|
5266
5586
|
"## Data Ownership",
|
|
5267
5587
|
"",
|
|
@@ -5269,14 +5589,14 @@ function internalDocs(application, options) {
|
|
|
5269
5589
|
"",
|
|
5270
5590
|
"## Deployments",
|
|
5271
5591
|
"",
|
|
5272
|
-
application.productionUrl ? `The planned production host is \`${application.productionUrl}\`. Deployment metadata starts in \`topolo.cloudcontrol.json\` and must stay aligned with CloudControl before live release.` : "No production host has been assigned yet. Deployment metadata must be completed before live release.",
|
|
5592
|
+
application.productionUrl ? `The planned production host is \`${application.productionUrl}\`. Staging mirrors the scaffold at \`https://${slug}.stg.topolo.us\` through the \`wrangler.toml\` staging environment and Topolo Staging Cloudflare account. Deployment metadata starts in \`topolo.cloudcontrol.json\` and must stay aligned with CloudControl before live release.` : "No production host has been assigned yet. Deployment metadata must be completed before live release.",
|
|
5273
5593
|
"",
|
|
5274
5594
|
"## Failure Modes",
|
|
5275
5595
|
"",
|
|
5276
5596
|
"- Auth service registration drifts from the generated Worker `SERVICE_ID`.",
|
|
5277
5597
|
"- CloudControl metadata does not match the eventual Cloudflare project or route.",
|
|
5278
5598
|
"- linked Topolo packages resolve a second React runtime and the browser shell renders blank instead of mounting the shared surfaces.",
|
|
5279
|
-
|
|
5599
|
+
`- ${boundaryFailureText}`,
|
|
5280
5600
|
"",
|
|
5281
5601
|
"## Debugging",
|
|
5282
5602
|
"",
|
|
@@ -5299,6 +5619,8 @@ function publicDocs(application, options) {
|
|
|
5299
5619
|
const title = toTitle(application.name);
|
|
5300
5620
|
const suiteLabel = options.audience === "personal" ? "personal" : "business";
|
|
5301
5621
|
const tenantLabel = options.tenancy === "household" ? "household" : "organization";
|
|
5622
|
+
const apiRouteLabel = options.tenancy === "household" ? "authenticated personal and household routes" : "authenticated workspace routes";
|
|
5623
|
+
const dataModelFailure = options.tenancy === "household" ? "household-scoped data models are not implemented until domain work begins" : "organization-scoped data models are not implemented until domain work begins";
|
|
5302
5624
|
return [
|
|
5303
5625
|
"---",
|
|
5304
5626
|
`title: "${escapeFrontmatter(title)}"`,
|
|
@@ -5334,7 +5656,7 @@ function publicDocs(application, options) {
|
|
|
5334
5656
|
"",
|
|
5335
5657
|
"## API Reference",
|
|
5336
5658
|
"",
|
|
5337
|
-
options.api ?
|
|
5659
|
+
options.api ? `The generated API baseline exposes ${apiRouteLabel} under \`/api/*\` and validates requests through Topolo Auth.` : "This generated baseline is browser-only and does not expose a callable API service.",
|
|
5338
5660
|
"",
|
|
5339
5661
|
"## Auth and Permissions",
|
|
5340
5662
|
"",
|
|
@@ -5346,12 +5668,12 @@ function publicDocs(application, options) {
|
|
|
5346
5668
|
"",
|
|
5347
5669
|
"## Deployments",
|
|
5348
5670
|
"",
|
|
5349
|
-
application.productionUrl ? `The planned production host is \`${application.productionUrl}\`.` : "The production host has not been assigned yet.",
|
|
5671
|
+
application.productionUrl ? `The planned production host is \`${application.productionUrl}\`. Staging mirrors the scaffold at \`https://${slug}.stg.topolo.us\`.` : "The production host has not been assigned yet.",
|
|
5350
5672
|
"",
|
|
5351
5673
|
"## Failure Modes",
|
|
5352
5674
|
"",
|
|
5353
5675
|
"- service registration and app routing can drift while the product is still planned",
|
|
5354
|
-
|
|
5676
|
+
`- ${dataModelFailure}`,
|
|
5355
5677
|
"",
|
|
5356
5678
|
"## Debugging",
|
|
5357
5679
|
"",
|
|
@@ -5360,7 +5682,7 @@ function publicDocs(application, options) {
|
|
|
5360
5682
|
"",
|
|
5361
5683
|
"## Change Log / Verification",
|
|
5362
5684
|
"",
|
|
5363
|
-
`- Scaffold generated on ${CURRENT_VERIFICATION_DATE} from
|
|
5685
|
+
`- Scaffold generated on ${CURRENT_VERIFICATION_DATE} from \`${options.portfolio ? "topolo platform apps scaffold" : "topolo apps scaffold"}\`.`,
|
|
5364
5686
|
""
|
|
5365
5687
|
].join("\n");
|
|
5366
5688
|
}
|
|
@@ -5369,8 +5691,17 @@ function serviceManifestJson(application, options) {
|
|
|
5369
5691
|
const title = toTitle(application.name);
|
|
5370
5692
|
const permissionPrefix = application.id.replace(/[^a-z0-9_]/g, "_");
|
|
5371
5693
|
const resourceLabel = options.tenancy === "household" ? "household" : "workspace";
|
|
5694
|
+
const launcherSettings = options.tenancy === "household" ? {
|
|
5695
|
+
settings: {
|
|
5696
|
+
launcher: {
|
|
5697
|
+
supported_contexts: ["personal"],
|
|
5698
|
+
household_capable: true
|
|
5699
|
+
}
|
|
5700
|
+
}
|
|
5701
|
+
} : {};
|
|
5372
5702
|
return `${json({
|
|
5373
5703
|
serviceId: application.serviceId,
|
|
5704
|
+
...launcherSettings,
|
|
5374
5705
|
systems: [
|
|
5375
5706
|
{
|
|
5376
5707
|
systemId: `topolo-${slug}`,
|
|
@@ -5569,7 +5900,6 @@ function escapeFrontmatter(value) {
|
|
|
5569
5900
|
}
|
|
5570
5901
|
|
|
5571
5902
|
// src/commands/app-scaffold-provision.ts
|
|
5572
|
-
var DEFAULT_DEVELOPERS_ORIGIN = "https://developers.topolo.app";
|
|
5573
5903
|
var DEVELOPERS_SERVICE_ID = "svc_topolo_developers";
|
|
5574
5904
|
async function provisionApplicationScaffold(payload) {
|
|
5575
5905
|
if (!payload.application.productionUrl) {
|
|
@@ -5582,7 +5912,8 @@ async function provisionApplicationScaffold(payload) {
|
|
|
5582
5912
|
"Platform scaffold provisioning requires a service id. Re-run without --no-api or pass --service."
|
|
5583
5913
|
);
|
|
5584
5914
|
}
|
|
5585
|
-
const
|
|
5915
|
+
const environment = selectedEnvironment();
|
|
5916
|
+
const credential = await refreshCredentialIfNeeded(environment);
|
|
5586
5917
|
if (!credential) {
|
|
5587
5918
|
throw new Error(
|
|
5588
5919
|
"topolo apps scaffold --provision requires an authenticated Topolo session. Run `topolo auth login` first."
|
|
@@ -5596,15 +5927,16 @@ async function provisionApplicationScaffold(payload) {
|
|
|
5596
5927
|
}
|
|
5597
5928
|
const identity = await client.client.introspect();
|
|
5598
5929
|
const config = await readConfig();
|
|
5599
|
-
const
|
|
5600
|
-
const developersOrigin = sanitizeOrigin(serviceUrls
|
|
5930
|
+
const profile = resolvedProfileConfig(config, environment);
|
|
5931
|
+
const developersOrigin = sanitizeOrigin(profile.serviceUrls?.developers ?? "https://developers.topolo.app");
|
|
5932
|
+
assertServiceUrlForEnvironment("developers", developersOrigin, environment);
|
|
5601
5933
|
const endpoint = payload.target === "platform" ? "/api/developer-console/admin/first-party-app-scaffolds" : "/api/developer-console/apps/scaffold";
|
|
5602
5934
|
const headers = new Headers({
|
|
5603
5935
|
"Content-Type": "application/json",
|
|
5604
5936
|
"X-Service-ID": DEVELOPERS_SERVICE_ID,
|
|
5605
5937
|
"X-Topolo-Client": `topolo-cli/${CLI_VERSION}`
|
|
5606
5938
|
});
|
|
5607
|
-
const agentName = resolveAgentName(
|
|
5939
|
+
const agentName = resolveAgentName(profile.agentName);
|
|
5608
5940
|
if (agentName) {
|
|
5609
5941
|
headers.set("X-Topolo-Agent", agentName);
|
|
5610
5942
|
}
|
|
@@ -6361,39 +6693,48 @@ function registerServicesCommand(program2) {
|
|
|
6361
6693
|
program2.command("services").description("List every Topolo service the SDK can route to").option("--json", "Output JSON", true).option("--no-json", "Output human-readable table").action(async (opts) => {
|
|
6362
6694
|
const mode = resolveOutputMode(opts.json);
|
|
6363
6695
|
try {
|
|
6696
|
+
const environment = selectedEnvironment();
|
|
6364
6697
|
const config = await readConfig();
|
|
6365
|
-
const rows = buildRows(
|
|
6366
|
-
emit({ services: rows }, mode, () => formatRows2(rows));
|
|
6698
|
+
const rows = buildRows(config.serviceUrls, environment);
|
|
6699
|
+
emit({ environment, services: rows }, mode, () => formatRows2(rows, environment));
|
|
6367
6700
|
} catch (err) {
|
|
6368
6701
|
fail(err, mode);
|
|
6369
6702
|
}
|
|
6370
6703
|
});
|
|
6371
6704
|
}
|
|
6372
|
-
function buildRows(overrides) {
|
|
6705
|
+
function buildRows(overrides, environment = selectedEnvironment()) {
|
|
6706
|
+
const resolvedOverrides = resolveServiceUrls(overrides, environment);
|
|
6373
6707
|
return Object.keys(DEFAULT_SERVICE_URLS).sort().map((id) => {
|
|
6374
6708
|
const defaultUrl = DEFAULT_SERVICE_URLS[id];
|
|
6375
|
-
const effectiveUrl = resolveServiceUrl(id,
|
|
6709
|
+
const effectiveUrl = resolveServiceUrl(id, resolvedOverrides);
|
|
6376
6710
|
return {
|
|
6377
6711
|
id,
|
|
6378
6712
|
defaultUrl,
|
|
6379
6713
|
effectiveUrl,
|
|
6380
|
-
overridden: defaultUrl !== effectiveUrl
|
|
6714
|
+
overridden: defaultUrl !== effectiveUrl,
|
|
6715
|
+
environmentSafe: environment !== "staging" || isStagingUrl(effectiveUrl)
|
|
6381
6716
|
};
|
|
6382
6717
|
});
|
|
6383
6718
|
}
|
|
6384
|
-
function formatRows2(rows) {
|
|
6719
|
+
function formatRows2(rows, environment = selectedEnvironment()) {
|
|
6385
6720
|
const idWidth = Math.max(3, ...rows.map((r) => r.id.length));
|
|
6386
6721
|
const urlWidth = Math.max(7, ...rows.map((r) => r.effectiveUrl.length));
|
|
6387
6722
|
const lines = [];
|
|
6388
|
-
lines.push(
|
|
6389
|
-
lines.push(
|
|
6723
|
+
lines.push(`Environment: ${environment}`);
|
|
6724
|
+
lines.push("");
|
|
6725
|
+
lines.push(`${"ID".padEnd(idWidth)} ${"URL".padEnd(urlWidth)} OVERRIDE READY`);
|
|
6726
|
+
lines.push(`${"-".repeat(idWidth)} ${"-".repeat(urlWidth)} -------- -----`);
|
|
6390
6727
|
for (const r of rows) {
|
|
6391
6728
|
lines.push(
|
|
6392
|
-
`${r.id.padEnd(idWidth)} ${r.effectiveUrl.padEnd(urlWidth)} ${r.overridden ? "yes" : ""}`
|
|
6729
|
+
`${r.id.padEnd(idWidth)} ${r.effectiveUrl.padEnd(urlWidth)} ${r.overridden ? "yes" : "".padEnd(8)} ${r.environmentSafe ? "yes" : "no"}`
|
|
6393
6730
|
);
|
|
6394
6731
|
}
|
|
6395
6732
|
return lines.join("\n");
|
|
6396
6733
|
}
|
|
6734
|
+
function isStagingUrl(value) {
|
|
6735
|
+
const url = value.toLowerCase();
|
|
6736
|
+
return url.includes(".stg.topolo.us") || url.includes("localhost") || url.includes("127.0.0.1") || url.includes("topolo-staging");
|
|
6737
|
+
}
|
|
6397
6738
|
|
|
6398
6739
|
// src/commands/seed.ts
|
|
6399
6740
|
var DEFAULT_SEED_BASE_URL = "https://seed.stg.topolo.us";
|
|
@@ -6401,9 +6742,9 @@ function seedBaseUrl() {
|
|
|
6401
6742
|
return process.env.TOPOLO_SEED_URL || DEFAULT_SEED_BASE_URL;
|
|
6402
6743
|
}
|
|
6403
6744
|
async function authedFetch(path4, init = {}) {
|
|
6404
|
-
const credential = await refreshCredentialIfNeeded();
|
|
6745
|
+
const credential = await refreshCredentialIfNeeded("staging");
|
|
6405
6746
|
if (!credential) {
|
|
6406
|
-
throw new Error("No credential configured. Run `topolo auth login` or set
|
|
6747
|
+
throw new Error("No staging credential configured. Run `topolo auth login --env staging` or set TOPOLO_STAGING_API_KEY.");
|
|
6407
6748
|
}
|
|
6408
6749
|
const headers = new Headers(init.headers || {});
|
|
6409
6750
|
if (credential.kind === "api_key") headers.set("X-Api-Key", credential.apiKey);
|
|
@@ -6611,9 +6952,22 @@ function formatManifestsTable(manifests) {
|
|
|
6611
6952
|
|
|
6612
6953
|
// src/index.ts
|
|
6613
6954
|
var program = new Command();
|
|
6955
|
+
var argv;
|
|
6956
|
+
try {
|
|
6957
|
+
argv = consumeEnvironmentArg(process.argv);
|
|
6958
|
+
} catch (err) {
|
|
6959
|
+
process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}
|
|
6960
|
+
`);
|
|
6961
|
+
process.exit(1);
|
|
6962
|
+
}
|
|
6614
6963
|
program.name("topolo").description(
|
|
6615
6964
|
"Topolo CLI. Public commands are scoped to the authenticated organization or developer workspace, and first-party Topolo scaffolding lives under the internal `topolo platform` namespace."
|
|
6616
|
-
).version(CLI_VERSION);
|
|
6965
|
+
).version(CLI_VERSION).option("--env <environment>", "Topolo environment: production or staging");
|
|
6966
|
+
program.hook("preAction", (thisCommand) => {
|
|
6967
|
+
const opts = thisCommand.opts();
|
|
6968
|
+
if (!opts.env) return;
|
|
6969
|
+
process.env.TOPOLO_ENV = normalizeEnvironment(opts.env);
|
|
6970
|
+
});
|
|
6617
6971
|
registerAuthCommands(program);
|
|
6618
6972
|
registerWhoamiCommand(program);
|
|
6619
6973
|
registerCrmCommands(program);
|
|
@@ -6622,8 +6976,30 @@ registerAppsCommand(program);
|
|
|
6622
6976
|
registerPlatformCommand(program);
|
|
6623
6977
|
registerServicesCommand(program);
|
|
6624
6978
|
registerSeedCommands(program);
|
|
6625
|
-
program.parseAsync(
|
|
6979
|
+
program.parseAsync(argv).catch((err) => {
|
|
6626
6980
|
process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}
|
|
6627
6981
|
`);
|
|
6628
6982
|
process.exit(1);
|
|
6629
6983
|
});
|
|
6984
|
+
function consumeEnvironmentArg(argv2) {
|
|
6985
|
+
const next = argv2.slice(0, 2);
|
|
6986
|
+
for (let i = 2; i < argv2.length; i += 1) {
|
|
6987
|
+
const arg = argv2[i];
|
|
6988
|
+
if (!arg) continue;
|
|
6989
|
+
if (arg === "--env") {
|
|
6990
|
+
const value = argv2[i + 1];
|
|
6991
|
+
if (!value || value.startsWith("-")) {
|
|
6992
|
+
throw new Error("--env requires production or staging.");
|
|
6993
|
+
}
|
|
6994
|
+
process.env.TOPOLO_ENV = normalizeEnvironment(value);
|
|
6995
|
+
i += 1;
|
|
6996
|
+
continue;
|
|
6997
|
+
}
|
|
6998
|
+
if (arg.startsWith("--env=")) {
|
|
6999
|
+
process.env.TOPOLO_ENV = normalizeEnvironment(arg.slice("--env=".length));
|
|
7000
|
+
continue;
|
|
7001
|
+
}
|
|
7002
|
+
next.push(arg);
|
|
7003
|
+
}
|
|
7004
|
+
return next;
|
|
7005
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Command-line interface for Topolo developers, operators, and agents, with internal first-party scaffolding available under the topolo platform namespace.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"commander": "^12.1.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@topolo/sdk": "^0.2.
|
|
24
|
+
"@topolo/sdk": "^0.2.11",
|
|
25
25
|
"@types/node": "^20.12.0",
|
|
26
26
|
"tsup": "^8.0.0",
|
|
27
27
|
"typescript": "^5.4.0",
|