@topolo/cli 0.1.16 → 0.1.17
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 +467 -95
- package/package.json +1 -1
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
|
@@ -9,7 +9,7 @@ import { platform as platform2 } from "os";
|
|
|
9
9
|
import { createInterface } from "readline/promises";
|
|
10
10
|
import { stdin as input } from "process";
|
|
11
11
|
|
|
12
|
-
//
|
|
12
|
+
// ../packages/topolo-sdk/dist/index.js
|
|
13
13
|
function applyAuthHeaders(headers, credential) {
|
|
14
14
|
if (credential.kind === "api_key") {
|
|
15
15
|
headers.set("X-Api-Key", credential.apiKey);
|
|
@@ -75,6 +75,7 @@ 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",
|
|
80
81
|
director: "https://director.topolo.app",
|
|
@@ -82,6 +83,7 @@ var SERVICES = {
|
|
|
82
83
|
flow: "https://flow.topolo.app",
|
|
83
84
|
forecast: "https://forecast-api.topolo.app",
|
|
84
85
|
forms: "https://forms.topolo.app",
|
|
86
|
+
home: "https://home.topolo.app",
|
|
85
87
|
insights: "https://insights.topolo.app",
|
|
86
88
|
inventory: "https://inventory.topolo.app",
|
|
87
89
|
learn: "https://learn-api.topolo.app",
|
|
@@ -112,11 +114,13 @@ var PLATFORM_SERVICE_IDS = {
|
|
|
112
114
|
books: "srv_topolo_books",
|
|
113
115
|
bugfix: "srv_topolo_bugfix",
|
|
114
116
|
compose: "srv_topolo_compose",
|
|
117
|
+
consent: "srv_topolo_consent",
|
|
115
118
|
crm: "srv_iCwM4jGXcwlj",
|
|
116
119
|
customer_success: "srv_topolo_success",
|
|
117
120
|
director: "srv_topolo_director",
|
|
118
121
|
flow: "srv_topolo_flow",
|
|
119
122
|
forms: "srv_topolo_forms",
|
|
123
|
+
home: "srv_topolo_home",
|
|
120
124
|
insights: "srv_topolo_insights",
|
|
121
125
|
inventory: "srv_topolo_inventory",
|
|
122
126
|
localize: "srv_topolo_localize",
|
|
@@ -449,6 +453,30 @@ var APPLICATIONS = {
|
|
|
449
453
|
}
|
|
450
454
|
]
|
|
451
455
|
},
|
|
456
|
+
"consent": {
|
|
457
|
+
"id": "consent",
|
|
458
|
+
"appDir": "TopoloConsent",
|
|
459
|
+
"name": "topolo-consent",
|
|
460
|
+
"packageName": "topolo-consent",
|
|
461
|
+
"productionUrl": "https://consent.topolo.app",
|
|
462
|
+
"services": [
|
|
463
|
+
"consent"
|
|
464
|
+
],
|
|
465
|
+
"deployTargets": [
|
|
466
|
+
{
|
|
467
|
+
"name": "topolo-consent",
|
|
468
|
+
"kind": "worker",
|
|
469
|
+
"apiUrl": "https://consent.topolo.app",
|
|
470
|
+
"domains": [
|
|
471
|
+
"consent.topolo.app"
|
|
472
|
+
],
|
|
473
|
+
"routes": [
|
|
474
|
+
"consent.topolo.app"
|
|
475
|
+
],
|
|
476
|
+
"serviceId": "consent"
|
|
477
|
+
}
|
|
478
|
+
]
|
|
479
|
+
},
|
|
452
480
|
"crm": {
|
|
453
481
|
"id": "crm",
|
|
454
482
|
"appDir": "TopoloCRM",
|
|
@@ -711,6 +739,30 @@ var APPLICATIONS = {
|
|
|
711
739
|
}
|
|
712
740
|
]
|
|
713
741
|
},
|
|
742
|
+
"home": {
|
|
743
|
+
"id": "home",
|
|
744
|
+
"appDir": "TopoloHome",
|
|
745
|
+
"name": "topolo-home",
|
|
746
|
+
"packageName": "topolo-home",
|
|
747
|
+
"productionUrl": "https://home.topolo.app",
|
|
748
|
+
"services": [
|
|
749
|
+
"home"
|
|
750
|
+
],
|
|
751
|
+
"deployTargets": [
|
|
752
|
+
{
|
|
753
|
+
"name": "topolo-home",
|
|
754
|
+
"kind": "worker",
|
|
755
|
+
"apiUrl": "https://home.topolo.app",
|
|
756
|
+
"domains": [
|
|
757
|
+
"home.topolo.app"
|
|
758
|
+
],
|
|
759
|
+
"routes": [
|
|
760
|
+
"home.topolo.app"
|
|
761
|
+
],
|
|
762
|
+
"serviceId": "home"
|
|
763
|
+
}
|
|
764
|
+
]
|
|
765
|
+
},
|
|
714
766
|
"insights": {
|
|
715
767
|
"id": "insights",
|
|
716
768
|
"appDir": "TopoloInsights",
|
|
@@ -2167,6 +2219,16 @@ import { mkdir, readFile, writeFile, chmod } from "fs/promises";
|
|
|
2167
2219
|
import { existsSync } from "fs";
|
|
2168
2220
|
import { homedir, platform } from "os";
|
|
2169
2221
|
import { dirname, join } from "path";
|
|
2222
|
+
var ENVIRONMENT_DEFAULT_SERVICE_URLS = {
|
|
2223
|
+
production: {
|
|
2224
|
+
developers: "https://developers.topolo.app"
|
|
2225
|
+
},
|
|
2226
|
+
staging: {
|
|
2227
|
+
auth: "https://auth.stg.topolo.us",
|
|
2228
|
+
developers: "https://developers.stg.topolo.us",
|
|
2229
|
+
seed: "https://seed.stg.topolo.us"
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2170
2232
|
function configPath() {
|
|
2171
2233
|
const override = process.env.TOPOLO_CONFIG_PATH;
|
|
2172
2234
|
if (override) return override;
|
|
@@ -2194,31 +2256,113 @@ async function writeConfig(config) {
|
|
|
2194
2256
|
}
|
|
2195
2257
|
}
|
|
2196
2258
|
}
|
|
2197
|
-
|
|
2198
|
-
const
|
|
2259
|
+
function normalizeEnvironment(value) {
|
|
2260
|
+
const normalized = (value ?? "production").trim().toLowerCase();
|
|
2261
|
+
if (normalized === "" || normalized === "production" || normalized === "prod") {
|
|
2262
|
+
return "production";
|
|
2263
|
+
}
|
|
2264
|
+
if (normalized === "staging" || normalized === "stage" || normalized === "stg") {
|
|
2265
|
+
return "staging";
|
|
2266
|
+
}
|
|
2267
|
+
throw new Error(`Unknown Topolo environment "${value}". Expected production or staging.`);
|
|
2268
|
+
}
|
|
2269
|
+
function selectedEnvironment() {
|
|
2270
|
+
return normalizeEnvironment(process.env.TOPOLO_ENV);
|
|
2271
|
+
}
|
|
2272
|
+
function envVarPrefix(environment) {
|
|
2273
|
+
return environment === "production" ? "TOPOLO_PRODUCTION" : "TOPOLO_STAGING";
|
|
2274
|
+
}
|
|
2275
|
+
function environmentLabel(environment = selectedEnvironment()) {
|
|
2276
|
+
return environment === "production" ? "production" : "staging";
|
|
2277
|
+
}
|
|
2278
|
+
function developerConsoleUrl(environment = selectedEnvironment()) {
|
|
2279
|
+
return environment === "production" ? "https://developers.topolo.app" : "https://developers.stg.topolo.us";
|
|
2280
|
+
}
|
|
2281
|
+
function oauthClientRegistrationUrl(environment = selectedEnvironment()) {
|
|
2282
|
+
return `${developerConsoleUrl(environment)}/developers/oauth-clients`;
|
|
2283
|
+
}
|
|
2284
|
+
function apiKeyIssuanceUrl(environment = selectedEnvironment()) {
|
|
2285
|
+
return `${developerConsoleUrl(environment)}/credentials`;
|
|
2286
|
+
}
|
|
2287
|
+
function profileForEnvironment(config, environment = selectedEnvironment()) {
|
|
2288
|
+
const profile = config.profiles?.[environment] ?? {};
|
|
2289
|
+
if (environment === "production") {
|
|
2290
|
+
return {
|
|
2291
|
+
credential: config.credential,
|
|
2292
|
+
oauthClientId: config.oauthClientId,
|
|
2293
|
+
...profile,
|
|
2294
|
+
serviceUrls: {
|
|
2295
|
+
...config.serviceUrls ?? {},
|
|
2296
|
+
...profile.serviceUrls ?? {}
|
|
2297
|
+
},
|
|
2298
|
+
agentName: profile.agentName ?? config.agentName
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
return profile;
|
|
2302
|
+
}
|
|
2303
|
+
function writeProfileConfig(config, environment, profile) {
|
|
2304
|
+
const profiles = { ...config.profiles ?? {} };
|
|
2305
|
+
profiles[environment] = {
|
|
2306
|
+
...profiles[environment] ?? {},
|
|
2307
|
+
...profile
|
|
2308
|
+
};
|
|
2309
|
+
return { ...config, profiles };
|
|
2310
|
+
}
|
|
2311
|
+
function clearProfileCredential(config, environment) {
|
|
2312
|
+
const next = { ...config };
|
|
2313
|
+
if (environment === "production") {
|
|
2314
|
+
delete next.credential;
|
|
2315
|
+
delete next.oauthClientId;
|
|
2316
|
+
}
|
|
2317
|
+
const profiles = { ...config.profiles ?? {} };
|
|
2318
|
+
const profile = { ...profiles[environment] ?? {} };
|
|
2319
|
+
delete profile.credential;
|
|
2320
|
+
delete profile.oauthClientId;
|
|
2321
|
+
profiles[environment] = profile;
|
|
2322
|
+
return { ...next, profiles };
|
|
2323
|
+
}
|
|
2324
|
+
function resolveOAuthClientId(config, environment = selectedEnvironment()) {
|
|
2325
|
+
const envSpecific = process.env[`${envVarPrefix(environment)}_CLI_CLIENT_ID`];
|
|
2326
|
+
if (envSpecific) return envSpecific;
|
|
2327
|
+
if (process.env.TOPOLO_CLI_CLIENT_ID) return process.env.TOPOLO_CLI_CLIENT_ID;
|
|
2328
|
+
return profileForEnvironment(config, environment).oauthClientId;
|
|
2329
|
+
}
|
|
2330
|
+
function credentialSource(config, environment = selectedEnvironment()) {
|
|
2331
|
+
const prefix = envVarPrefix(environment);
|
|
2332
|
+
if (process.env[`${prefix}_API_KEY`]) return `env:${prefix}_API_KEY`;
|
|
2333
|
+
if (process.env[`${prefix}_ACCESS_TOKEN`]) return `env:${prefix}_ACCESS_TOKEN`;
|
|
2334
|
+
if (process.env.TOPOLO_API_KEY) return "env:TOPOLO_API_KEY";
|
|
2335
|
+
if (process.env.TOPOLO_ACCESS_TOKEN) return "env:TOPOLO_ACCESS_TOKEN";
|
|
2336
|
+
return profileForEnvironment(config, environment).credential ? "config-file" : "none";
|
|
2337
|
+
}
|
|
2338
|
+
async function resolveCredential(environment = selectedEnvironment()) {
|
|
2339
|
+
const prefix = envVarPrefix(environment);
|
|
2340
|
+
const apiKey = process.env[`${prefix}_API_KEY`] ?? process.env.TOPOLO_API_KEY;
|
|
2199
2341
|
if (apiKey) return { kind: "api_key", apiKey };
|
|
2200
|
-
const accessToken = process.env.TOPOLO_ACCESS_TOKEN;
|
|
2342
|
+
const accessToken = process.env[`${prefix}_ACCESS_TOKEN`] ?? process.env.TOPOLO_ACCESS_TOKEN;
|
|
2201
2343
|
if (accessToken) return { kind: "access_token", accessToken };
|
|
2202
2344
|
const config = await readConfig();
|
|
2203
|
-
return config.credential ?? null;
|
|
2345
|
+
return profileForEnvironment(config, environment).credential ?? null;
|
|
2204
2346
|
}
|
|
2205
2347
|
var REFRESH_SKEW_SECONDS = 60;
|
|
2206
|
-
async function refreshCredentialIfNeeded() {
|
|
2207
|
-
|
|
2208
|
-
|
|
2348
|
+
async function refreshCredentialIfNeeded(environment = selectedEnvironment()) {
|
|
2349
|
+
const prefix = envVarPrefix(environment);
|
|
2350
|
+
if (process.env[`${prefix}_API_KEY`] || process.env[`${prefix}_ACCESS_TOKEN`] || process.env.TOPOLO_API_KEY || process.env.TOPOLO_ACCESS_TOKEN) {
|
|
2351
|
+
return resolveCredential(environment);
|
|
2209
2352
|
}
|
|
2210
2353
|
const config = await readConfig();
|
|
2211
|
-
const
|
|
2354
|
+
const profile = profileForEnvironment(config, environment);
|
|
2355
|
+
const cred = profile.credential;
|
|
2212
2356
|
if (!cred || cred.kind !== "access_token") return cred ?? null;
|
|
2213
|
-
if (!cred.refreshToken || !cred.expiresAt || !
|
|
2357
|
+
if (!cred.refreshToken || !cred.expiresAt || !profile.oauthClientId) return cred;
|
|
2214
2358
|
const now = Math.floor(Date.now() / 1e3);
|
|
2215
2359
|
if (cred.expiresAt - REFRESH_SKEW_SECONDS > now) return cred;
|
|
2216
2360
|
try {
|
|
2217
2361
|
const oauth = new TopoloOAuth({
|
|
2218
|
-
serviceUrls: resolveServiceUrls(
|
|
2362
|
+
serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
|
|
2219
2363
|
});
|
|
2220
2364
|
const tokens = await oauth.refreshToken({
|
|
2221
|
-
clientId:
|
|
2365
|
+
clientId: profile.oauthClientId,
|
|
2222
2366
|
refreshToken: cred.refreshToken
|
|
2223
2367
|
});
|
|
2224
2368
|
const refreshed = {
|
|
@@ -2227,7 +2371,9 @@ async function refreshCredentialIfNeeded() {
|
|
|
2227
2371
|
...tokens.refresh_token ? { refreshToken: tokens.refresh_token } : { refreshToken: cred.refreshToken },
|
|
2228
2372
|
expiresAt: Math.floor(Date.now() / 1e3) + tokens.expires_in
|
|
2229
2373
|
};
|
|
2230
|
-
await writeConfig(
|
|
2374
|
+
await writeConfig(
|
|
2375
|
+
environment === "production" && !config.profiles?.production ? { ...config, credential: refreshed } : writeProfileConfig(config, environment, { credential: refreshed })
|
|
2376
|
+
);
|
|
2231
2377
|
return refreshed;
|
|
2232
2378
|
} catch {
|
|
2233
2379
|
return cred;
|
|
@@ -2236,10 +2382,17 @@ async function refreshCredentialIfNeeded() {
|
|
|
2236
2382
|
function resolveAgentName(configAgentName) {
|
|
2237
2383
|
return process.env.TOPOLO_AGENT_NAME ?? configAgentName;
|
|
2238
2384
|
}
|
|
2239
|
-
function resolveServiceUrls(configUrls) {
|
|
2385
|
+
function resolveServiceUrls(configUrls, environment = selectedEnvironment()) {
|
|
2240
2386
|
const merged = {
|
|
2387
|
+
...ENVIRONMENT_DEFAULT_SERVICE_URLS[environment],
|
|
2241
2388
|
...configUrls ?? {}
|
|
2242
2389
|
};
|
|
2390
|
+
const prefix = `${envVarPrefix(environment)}_SERVICE_URL_`;
|
|
2391
|
+
for (const [k, v] of Object.entries(process.env)) {
|
|
2392
|
+
if (k.startsWith(prefix) && v) {
|
|
2393
|
+
merged[k.slice(prefix.length).toLowerCase()] = v;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2243
2396
|
for (const [k, v] of Object.entries(process.env)) {
|
|
2244
2397
|
if (k.startsWith("TOPOLO_SERVICE_URL_") && v) {
|
|
2245
2398
|
merged[k.slice("TOPOLO_SERVICE_URL_".length).toLowerCase()] = v;
|
|
@@ -2247,6 +2400,28 @@ function resolveServiceUrls(configUrls) {
|
|
|
2247
2400
|
}
|
|
2248
2401
|
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
2249
2402
|
}
|
|
2403
|
+
function resolvedProfileConfig(config, environment = selectedEnvironment()) {
|
|
2404
|
+
const profile = profileForEnvironment(config, environment);
|
|
2405
|
+
return {
|
|
2406
|
+
credential: profile.credential,
|
|
2407
|
+
oauthClientId: profile.oauthClientId,
|
|
2408
|
+
agentName: profile.agentName,
|
|
2409
|
+
serviceUrls: resolveServiceUrls(profile.serviceUrls, environment)
|
|
2410
|
+
};
|
|
2411
|
+
}
|
|
2412
|
+
function assertServiceUrlForEnvironment(service, serviceUrl, environment = selectedEnvironment()) {
|
|
2413
|
+
if (environment !== "staging") return;
|
|
2414
|
+
if (!serviceUrl) {
|
|
2415
|
+
throw new Error(`No staging URL is configured for Topolo service "${service}".`);
|
|
2416
|
+
}
|
|
2417
|
+
const url = serviceUrl.toLowerCase();
|
|
2418
|
+
if (url.includes(".stg.topolo.us") || url.includes("localhost") || url.includes("127.0.0.1") || url.includes("topolo-staging")) {
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
throw new Error(
|
|
2422
|
+
`Refusing to call ${service} in staging because its effective URL is not a staging endpoint: ${serviceUrl}`
|
|
2423
|
+
);
|
|
2424
|
+
}
|
|
2250
2425
|
|
|
2251
2426
|
// src/output.ts
|
|
2252
2427
|
function resolveOutputMode(override) {
|
|
@@ -2293,38 +2468,38 @@ function normalizeError(err) {
|
|
|
2293
2468
|
}
|
|
2294
2469
|
|
|
2295
2470
|
// 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
2471
|
function registerAuthCommands(program2) {
|
|
2299
2472
|
const auth = program2.command("auth").description("Manage Topolo credentials");
|
|
2300
2473
|
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
2474
|
async (opts) => {
|
|
2302
2475
|
const mode = resolveOutputMode(opts.json);
|
|
2476
|
+
const environment = selectedEnvironment();
|
|
2303
2477
|
try {
|
|
2304
2478
|
if (opts.apiKey) {
|
|
2305
|
-
await storeApiKey(opts.apiKey.trim(), mode);
|
|
2479
|
+
await storeApiKey(opts.apiKey.trim(), environment, mode);
|
|
2306
2480
|
return;
|
|
2307
2481
|
}
|
|
2308
|
-
const
|
|
2482
|
+
const config = await readConfig();
|
|
2483
|
+
const clientId = opts.clientId ?? resolveOAuthClientId(config, environment);
|
|
2309
2484
|
if (!clientId) {
|
|
2310
2485
|
if (!input.isTTY) {
|
|
2311
2486
|
throw new Error(
|
|
2312
|
-
`No OAuth client_id configured and stdin is not a TTY. Either register an OAuth client at ${
|
|
2487
|
+
`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
2488
|
);
|
|
2314
2489
|
}
|
|
2315
2490
|
process.stderr.write(
|
|
2316
|
-
`No OAuth client_id configured. Falling back to API key.
|
|
2317
|
-
Visit ${
|
|
2491
|
+
`No OAuth client_id configured for ${environmentLabel(environment)}. Falling back to API key.
|
|
2492
|
+
Visit ${apiKeyIssuanceUrl(environment)} to mint one, or configure an OAuth client at ${oauthClientRegistrationUrl(environment)} and set the environment-specific CLI client id.
|
|
2318
2493
|
`
|
|
2319
2494
|
);
|
|
2320
2495
|
const rl = createInterface({ input, output: process.stderr });
|
|
2321
2496
|
const apiKey = (await rl.question("Paste API key: ")).trim();
|
|
2322
2497
|
rl.close();
|
|
2323
2498
|
if (!apiKey) throw new Error("API key is required");
|
|
2324
|
-
await storeApiKey(apiKey, mode);
|
|
2499
|
+
await storeApiKey(apiKey, environment, mode);
|
|
2325
2500
|
return;
|
|
2326
2501
|
}
|
|
2327
|
-
await runDeviceFlow({ clientId, scope: opts.scope, mode });
|
|
2502
|
+
await runDeviceFlow({ clientId, environment, scope: opts.scope, mode });
|
|
2328
2503
|
} catch (err) {
|
|
2329
2504
|
fail(err, mode);
|
|
2330
2505
|
}
|
|
@@ -2332,40 +2507,65 @@ Visit ${API_KEY_ISSUANCE_URL} to mint one, or configure an OAuth client at ${OAU
|
|
|
2332
2507
|
);
|
|
2333
2508
|
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
2509
|
const mode = resolveOutputMode(opts.json);
|
|
2510
|
+
const environment = selectedEnvironment();
|
|
2335
2511
|
try {
|
|
2336
2512
|
const config = await readConfig();
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2513
|
+
await writeConfig(clearProfileCredential(config, environment));
|
|
2514
|
+
emit(
|
|
2515
|
+
{ ok: true, environment },
|
|
2516
|
+
mode,
|
|
2517
|
+
() => `Logged out of Topolo ${environmentLabel(environment)}.`
|
|
2518
|
+
);
|
|
2340
2519
|
} catch (err) {
|
|
2341
2520
|
fail(err, mode);
|
|
2342
2521
|
}
|
|
2343
2522
|
});
|
|
2344
2523
|
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
2524
|
const mode = resolveOutputMode(opts.json);
|
|
2525
|
+
const environment = selectedEnvironment();
|
|
2346
2526
|
try {
|
|
2347
|
-
const
|
|
2348
|
-
|
|
2527
|
+
const config = await readConfig();
|
|
2528
|
+
const source = credentialSource(config, environment);
|
|
2529
|
+
const profile = resolvedProfileConfig(config, environment);
|
|
2530
|
+
emit(
|
|
2531
|
+
{
|
|
2532
|
+
environment,
|
|
2533
|
+
source,
|
|
2534
|
+
authUrl: profile.serviceUrls?.auth ?? null,
|
|
2535
|
+
developersUrl: profile.serviceUrls?.developers ?? null
|
|
2536
|
+
},
|
|
2537
|
+
mode,
|
|
2538
|
+
() => [
|
|
2539
|
+
`Environment: ${environmentLabel(environment)}`,
|
|
2540
|
+
`Credential source: ${source}`,
|
|
2541
|
+
`Auth URL: ${profile.serviceUrls?.auth ?? "(sdk default)"}`,
|
|
2542
|
+
`Developers URL: ${profile.serviceUrls?.developers ?? "(sdk default)"}`
|
|
2543
|
+
].join("\n")
|
|
2544
|
+
);
|
|
2349
2545
|
} catch (err) {
|
|
2350
2546
|
fail(err, mode);
|
|
2351
2547
|
}
|
|
2352
2548
|
});
|
|
2353
2549
|
}
|
|
2354
|
-
async function storeApiKey(apiKey, mode) {
|
|
2550
|
+
async function storeApiKey(apiKey, environment, mode) {
|
|
2355
2551
|
if (!apiKey) throw new Error("API key is required");
|
|
2356
2552
|
const config = await readConfig();
|
|
2357
|
-
|
|
2358
|
-
|
|
2553
|
+
await writeConfig(
|
|
2554
|
+
writeProfileConfig(config, environment, {
|
|
2555
|
+
credential: { kind: "api_key", apiKey }
|
|
2556
|
+
})
|
|
2557
|
+
);
|
|
2359
2558
|
emit(
|
|
2360
|
-
{ ok: true, credential: { kind: "api_key" } },
|
|
2559
|
+
{ ok: true, environment, credential: { kind: "api_key" } },
|
|
2361
2560
|
mode,
|
|
2362
|
-
() =>
|
|
2561
|
+
() => `Stored API key for Topolo ${environmentLabel(environment)}. Run \`topolo whoami\` to verify.`
|
|
2363
2562
|
);
|
|
2364
2563
|
}
|
|
2365
2564
|
async function runDeviceFlow(options) {
|
|
2366
2565
|
const config = await readConfig();
|
|
2566
|
+
const resolved = resolvedProfileConfig(config, options.environment);
|
|
2367
2567
|
const oauth = new TopoloOAuth({
|
|
2368
|
-
serviceUrls:
|
|
2568
|
+
serviceUrls: resolved.serviceUrls
|
|
2369
2569
|
});
|
|
2370
2570
|
const start = await oauth.requestDeviceCode({
|
|
2371
2571
|
clientId: options.clientId,
|
|
@@ -2377,7 +2577,7 @@ async function runDeviceFlow(options) {
|
|
|
2377
2577
|
if (options.mode === "pretty") {
|
|
2378
2578
|
process.stderr.write(
|
|
2379
2579
|
`
|
|
2380
|
-
Sign in to Topolo
|
|
2580
|
+
Sign in to Topolo ${environmentLabel(options.environment)}
|
|
2381
2581
|
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2382
2582
|
|
|
2383
2583
|
` + (opened ? ` Opening your browser to approve...
|
|
@@ -2405,17 +2605,21 @@ async function runDeviceFlow(options) {
|
|
|
2405
2605
|
spinner?.stop();
|
|
2406
2606
|
}
|
|
2407
2607
|
const updated = await readConfig();
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2608
|
+
await writeConfig(
|
|
2609
|
+
writeProfileConfig(updated, options.environment, {
|
|
2610
|
+
credential: {
|
|
2611
|
+
kind: "access_token",
|
|
2612
|
+
accessToken: token.access_token,
|
|
2613
|
+
...token.refresh_token ? { refreshToken: token.refresh_token } : {},
|
|
2614
|
+
expiresAt: Math.floor(Date.now() / 1e3) + token.expires_in
|
|
2615
|
+
},
|
|
2616
|
+
oauthClientId: options.clientId
|
|
2617
|
+
})
|
|
2618
|
+
);
|
|
2416
2619
|
emit(
|
|
2417
2620
|
{
|
|
2418
2621
|
ok: true,
|
|
2622
|
+
environment: options.environment,
|
|
2419
2623
|
credential: {
|
|
2420
2624
|
kind: "access_token",
|
|
2421
2625
|
expiresIn: token.expires_in,
|
|
@@ -2491,22 +2695,23 @@ function sleep(ms) {
|
|
|
2491
2695
|
}
|
|
2492
2696
|
|
|
2493
2697
|
// src/version.ts
|
|
2494
|
-
var CLI_VERSION = "0.1.
|
|
2698
|
+
var CLI_VERSION = "0.1.17";
|
|
2495
2699
|
|
|
2496
2700
|
// src/client.ts
|
|
2497
2701
|
async function buildClient(options) {
|
|
2498
|
-
const
|
|
2702
|
+
const environment = selectedEnvironment();
|
|
2703
|
+
const credential = await refreshCredentialIfNeeded(environment);
|
|
2499
2704
|
if (!credential) return null;
|
|
2500
2705
|
const config = await readConfig();
|
|
2501
|
-
const
|
|
2706
|
+
const profile = resolvedProfileConfig(config, environment);
|
|
2502
2707
|
return createTopolo({
|
|
2503
2708
|
credential,
|
|
2504
2709
|
agent: {
|
|
2505
2710
|
clientName: "topolo-cli",
|
|
2506
2711
|
clientVersion: CLI_VERSION,
|
|
2507
|
-
agentName: resolveAgentName(
|
|
2712
|
+
agentName: resolveAgentName(profile.agentName)
|
|
2508
2713
|
},
|
|
2509
|
-
serviceUrls,
|
|
2714
|
+
serviceUrls: profile.serviceUrls,
|
|
2510
2715
|
requireConfirmForWrites: options?.requireConfirmForWrites,
|
|
2511
2716
|
...isDebugEnabled() ? { debug: stderrDebugLogger } : {}
|
|
2512
2717
|
});
|
|
@@ -2543,6 +2748,9 @@ function registerWhoamiCommand(program2) {
|
|
|
2543
2748
|
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
2749
|
const mode = resolveOutputMode(opts.json);
|
|
2545
2750
|
try {
|
|
2751
|
+
const environment = selectedEnvironment();
|
|
2752
|
+
const profile = resolvedProfileConfig(await readConfig(), environment);
|
|
2753
|
+
assertServiceUrlForEnvironment("auth", profile.serviceUrls?.auth, environment);
|
|
2546
2754
|
const topolo = await buildClient();
|
|
2547
2755
|
if (!topolo) {
|
|
2548
2756
|
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
@@ -2573,6 +2781,7 @@ function registerCrmCommands(program2) {
|
|
|
2573
2781
|
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
2782
|
const mode = resolveOutputMode(opts.json);
|
|
2575
2783
|
try {
|
|
2784
|
+
await assertCrmRoutable();
|
|
2576
2785
|
const topolo = await buildClient();
|
|
2577
2786
|
if (!topolo) {
|
|
2578
2787
|
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
@@ -2590,6 +2799,7 @@ function registerCrmCommands(program2) {
|
|
|
2590
2799
|
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
2800
|
const mode = resolveOutputMode(opts.json);
|
|
2592
2801
|
try {
|
|
2802
|
+
await assertCrmRoutable();
|
|
2593
2803
|
const topolo = await buildClient();
|
|
2594
2804
|
if (!topolo) {
|
|
2595
2805
|
throw new Error("No credential configured. Run `topolo auth login` or set TOPOLO_API_KEY.");
|
|
@@ -2602,6 +2812,15 @@ function registerCrmCommands(program2) {
|
|
|
2602
2812
|
}
|
|
2603
2813
|
});
|
|
2604
2814
|
}
|
|
2815
|
+
async function assertCrmRoutable() {
|
|
2816
|
+
const environment = selectedEnvironment();
|
|
2817
|
+
const profile = resolvedProfileConfig(await readConfig(), environment);
|
|
2818
|
+
assertServiceUrlForEnvironment(
|
|
2819
|
+
"crm",
|
|
2820
|
+
resolveServiceUrl("crm", profile.serviceUrls),
|
|
2821
|
+
environment
|
|
2822
|
+
);
|
|
2823
|
+
}
|
|
2605
2824
|
function formatContactsTable(rows, total, page) {
|
|
2606
2825
|
if (rows.length === 0) return "No contacts found.";
|
|
2607
2826
|
const lines = [];
|
|
@@ -2660,6 +2879,14 @@ function registerApiCommand(program2) {
|
|
|
2660
2879
|
if (WRITE_METHODS2.has(httpMethod) && !opts.confirm) {
|
|
2661
2880
|
throw new Error(`Mutating ${httpMethod} requires --confirm.`);
|
|
2662
2881
|
}
|
|
2882
|
+
const environment = selectedEnvironment();
|
|
2883
|
+
const config = await readConfig();
|
|
2884
|
+
const profile = resolvedProfileConfig(config, environment);
|
|
2885
|
+
assertServiceUrlForEnvironment(
|
|
2886
|
+
service,
|
|
2887
|
+
resolveServiceUrl(service, profile.serviceUrls),
|
|
2888
|
+
environment
|
|
2889
|
+
);
|
|
2663
2890
|
let body = void 0;
|
|
2664
2891
|
if (opts.data) {
|
|
2665
2892
|
try {
|
|
@@ -3641,7 +3868,9 @@ import { constants } from "fs";
|
|
|
3641
3868
|
import { access, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
3642
3869
|
import { dirname as dirname2, relative, resolve } from "path";
|
|
3643
3870
|
var CLOUDFLARE_ACCOUNT_ID = "49ef1ba682ad8cfd720c86699ae17521";
|
|
3644
|
-
var
|
|
3871
|
+
var STAGING_CLOUDFLARE_ACCOUNT_ID = "4f4e1c69a3830946f9fea7b1eb7531ac";
|
|
3872
|
+
var CLOUDFLARE_COMPATIBILITY_DATE = "2026-05-05";
|
|
3873
|
+
var CURRENT_VERIFICATION_DATE = "2026-05-06";
|
|
3645
3874
|
function applicationScaffoldPayload(application, options = {}) {
|
|
3646
3875
|
const target = normalizeTarget(options.target);
|
|
3647
3876
|
const portfolio = target === "platform" ? normalizePortfolio(options.portfolio) : null;
|
|
@@ -4148,9 +4377,13 @@ function packageJson(application, target, options) {
|
|
|
4148
4377
|
if (target === "platform") {
|
|
4149
4378
|
dependencies["@topolo-io/auth-middleware"] = topoloPackageVersion("@topolo-io/auth-middleware", target);
|
|
4150
4379
|
dependencies["@topolo/sdk"] = topoloPackageVersion("@topolo/sdk", target);
|
|
4380
|
+
scripts.deploy = `node -e 'console.error("\\nX Bare deploy is disabled. Use deploy:staging or deploy:production.\\n"); process.exit(1)'`;
|
|
4381
|
+
scripts["deploy:staging"] = "DEPLOY_ENVIRONMENT=staging VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build && wrangler deploy --env staging";
|
|
4382
|
+
scripts["deploy:production"] = "DEPLOY_ENVIRONMENT=production npm run build && wrangler deploy";
|
|
4383
|
+
} else {
|
|
4384
|
+
scripts.deploy = "npm run build && wrangler deploy";
|
|
4151
4385
|
}
|
|
4152
4386
|
devDependencies.wrangler = "^4.40.2";
|
|
4153
|
-
scripts.deploy = "npm run build && wrangler deploy";
|
|
4154
4387
|
}
|
|
4155
4388
|
return `${json({
|
|
4156
4389
|
name: application.packageName,
|
|
@@ -4485,8 +4718,13 @@ function appTsx(application, target, options) {
|
|
|
4485
4718
|
function platformAppTsx(application, options) {
|
|
4486
4719
|
const appName = toTitle(application.name);
|
|
4487
4720
|
const serviceId = application.serviceId ?? application.id;
|
|
4488
|
-
const
|
|
4489
|
-
const
|
|
4721
|
+
const isPersonal = options.audience === "personal";
|
|
4722
|
+
const tagline = isPersonal ? "Topolo personal app" : "Topolo workspace";
|
|
4723
|
+
const workspaceTitle = isPersonal ? "Personal app" : "Workspace";
|
|
4724
|
+
const shellLabel = isPersonal ? "personal app shell" : "workspace shell";
|
|
4725
|
+
const signedSurfaceLabel = isPersonal ? "personal app" : "workspace";
|
|
4726
|
+
const baselineHeading = isPersonal ? "Shared personal baseline" : "Shared workspace baseline";
|
|
4727
|
+
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
4728
|
return [
|
|
4491
4729
|
"import { Component, useEffect, useMemo, useState, type ErrorInfo, type ReactNode } from 'react';",
|
|
4492
4730
|
"import { createBrowserAuthModule, type AuthUser } from '@topolo-io/auth-client';",
|
|
@@ -4512,17 +4750,17 @@ function platformAppTsx(application, options) {
|
|
|
4512
4750
|
" app_name: APP_NAME,",
|
|
4513
4751
|
` app_tagline: '${escapeSingleQuoted(tagline)}',`,
|
|
4514
4752
|
" hero_title: `Open ${APP_NAME} in Topolo`,",
|
|
4515
|
-
|
|
4753
|
+
` hero_subtitle: 'Shared landing, login, callback, auth, and ${escapeSingleQuoted(shellLabel)} wiring are already included in the generated baseline.',`,
|
|
4516
4754
|
" hero_cta_text: 'Sign in',",
|
|
4517
4755
|
" hero_cta_url: '/login',",
|
|
4518
4756
|
" cta_title: 'Start from the shared platform entry flow',",
|
|
4519
|
-
|
|
4757
|
+
` cta_subtitle: 'Unauthenticated routes stay on the shared Topolo surfaces before users enter the signed ${escapeSingleQuoted(shellLabel)}.',`,
|
|
4520
4758
|
"};",
|
|
4521
4759
|
"const LOGIN_CONFIG = {",
|
|
4522
4760
|
" app_name: APP_NAME,",
|
|
4523
4761
|
` app_tagline: '${escapeSingleQuoted(tagline)}',`,
|
|
4524
4762
|
" hero_title: `Sign in to ${APP_NAME}`,",
|
|
4525
|
-
|
|
4763
|
+
` hero_subtitle: 'Use Topolo Auth to open the shared ${escapeSingleQuoted(shellLabel)} for this application.',`,
|
|
4526
4764
|
" authenticated_home_path: '/',",
|
|
4527
4765
|
"};",
|
|
4528
4766
|
"",
|
|
@@ -4542,6 +4780,7 @@ function platformAppTsx(application, options) {
|
|
|
4542
4780
|
"function toShellUser(user: AuthUser | null): TopoloAppShellUser | null {",
|
|
4543
4781
|
" if (!user) return null;",
|
|
4544
4782
|
" return {",
|
|
4783
|
+
" id: user.id,",
|
|
4545
4784
|
" name: user.name ?? user.email,",
|
|
4546
4785
|
" email: user.email,",
|
|
4547
4786
|
" role: user.role,",
|
|
@@ -4690,7 +4929,7 @@ function platformAppTsx(application, options) {
|
|
|
4690
4929
|
" appId={APP_ID}",
|
|
4691
4930
|
" serviceId={SERVICE_ID}",
|
|
4692
4931
|
" appName={APP_NAME}",
|
|
4693
|
-
|
|
4932
|
+
` subtitle="Opening your ${escapeDoubleQuoted(signedSurfaceLabel)}..."`,
|
|
4694
4933
|
" />",
|
|
4695
4934
|
" );",
|
|
4696
4935
|
" }",
|
|
@@ -4701,6 +4940,9 @@ function platformAppTsx(application, options) {
|
|
|
4701
4940
|
" serviceId={SERVICE_ID}",
|
|
4702
4941
|
" appName={APP_NAME}",
|
|
4703
4942
|
" user={shellUser}",
|
|
4943
|
+
" orgId={user.activeContextType === 'organization' ? user.activeContextId ?? user.orgId ?? null : null}",
|
|
4944
|
+
" activeContextType={user.activeContextType}",
|
|
4945
|
+
" activeContextId={user.activeContextId}",
|
|
4704
4946
|
' activeNavItemId="home"',
|
|
4705
4947
|
` navItems={[{ id: 'home', label: '${escapeSingleQuoted(workspaceTitle)}' }]}`,
|
|
4706
4948
|
' navigationMode="topbar"',
|
|
@@ -4719,9 +4961,9 @@ function platformAppTsx(application, options) {
|
|
|
4719
4961
|
" >",
|
|
4720
4962
|
' <div className="topolo-starter-workspace">',
|
|
4721
4963
|
' <section className="topolo-starter-card">',
|
|
4722
|
-
|
|
4964
|
+
` <h2>${baselineHeading}</h2>`,
|
|
4723
4965
|
" <p>",
|
|
4724
|
-
|
|
4966
|
+
` ${baselineDescription}`,
|
|
4725
4967
|
" </p>",
|
|
4726
4968
|
" </section>",
|
|
4727
4969
|
" </div>",
|
|
@@ -4913,7 +5155,7 @@ function wranglerToml(application, target) {
|
|
|
4913
5155
|
const lines = [
|
|
4914
5156
|
`name = "${workerName}"`,
|
|
4915
5157
|
'main = "src/worker.ts"',
|
|
4916
|
-
`compatibility_date = "${
|
|
5158
|
+
`compatibility_date = "${CLOUDFLARE_COMPATIBILITY_DATE}"`,
|
|
4917
5159
|
'compatibility_flags = ["nodejs_compat"]'
|
|
4918
5160
|
];
|
|
4919
5161
|
if (target === "platform") {
|
|
@@ -4948,15 +5190,51 @@ function wranglerToml(application, target) {
|
|
|
4948
5190
|
`SERVICE_ID = "${application.serviceId ?? application.id}"`,
|
|
4949
5191
|
""
|
|
4950
5192
|
);
|
|
5193
|
+
if (target === "platform") {
|
|
5194
|
+
lines.push(
|
|
5195
|
+
"# Staging deploys use the separate Topolo Staging Cloudflare account.",
|
|
5196
|
+
"[env.staging]",
|
|
5197
|
+
`name = "${workerName}-staging"`,
|
|
5198
|
+
`account_id = "${STAGING_CLOUDFLARE_ACCOUNT_ID}"`,
|
|
5199
|
+
"workers_dev = true",
|
|
5200
|
+
"routes = [",
|
|
5201
|
+
` { pattern = "${slug}.stg.topolo.us", custom_domain = true }`,
|
|
5202
|
+
"]",
|
|
5203
|
+
"",
|
|
5204
|
+
"[env.staging.vars]",
|
|
5205
|
+
'AUTH_BASE_URL = "https://auth.stg.topolo.us"',
|
|
5206
|
+
`SERVICE_ID = "${application.serviceId ?? application.id}"`,
|
|
5207
|
+
""
|
|
5208
|
+
);
|
|
5209
|
+
}
|
|
4951
5210
|
return lines.join("\n");
|
|
4952
5211
|
}
|
|
4953
|
-
function workerTs(application, target,
|
|
5212
|
+
function workerTs(application, target, options) {
|
|
4954
5213
|
if (target === "developer") return developerWorkerTs(application);
|
|
4955
|
-
return platformWorkerTs(application);
|
|
5214
|
+
return platformWorkerTs(application, options);
|
|
4956
5215
|
}
|
|
4957
|
-
function platformWorkerTs(application) {
|
|
5216
|
+
function platformWorkerTs(application, options) {
|
|
4958
5217
|
const serviceId = application.serviceId ?? application.id;
|
|
4959
5218
|
const serviceName = toTitle(application.name);
|
|
5219
|
+
const openActionLabel = options.audience === "personal" ? "Open app" : "Open workspace";
|
|
5220
|
+
const bootstrapBody = options.tenancy === "household" ? [
|
|
5221
|
+
" const userContext = auth.user && typeof auth.user === 'object' ? (auth.user as unknown as Record<string, unknown>) : {};",
|
|
5222
|
+
" return json({",
|
|
5223
|
+
" user: auth.user,",
|
|
5224
|
+
" activeContextType: userContext.activeContextType ?? null,",
|
|
5225
|
+
" activeContextId: userContext.activeContextId ?? null,",
|
|
5226
|
+
" activeContext: userContext.activeContext ?? null,",
|
|
5227
|
+
" selectedHouseholdId: userContext.selectedHouseholdId ?? null,",
|
|
5228
|
+
" selectedHousehold: userContext.selectedHousehold ?? null,",
|
|
5229
|
+
" household: userContext.selectedHousehold ?? null,",
|
|
5230
|
+
" householdMemberships: userContext.householdMemberships ?? [],",
|
|
5231
|
+
" }, { status: 200 }, request);"
|
|
5232
|
+
] : [
|
|
5233
|
+
" return json({",
|
|
5234
|
+
" user: auth.user,",
|
|
5235
|
+
" organization: auth.organization,",
|
|
5236
|
+
" }, { status: 200 }, request);"
|
|
5237
|
+
];
|
|
4960
5238
|
return [
|
|
4961
5239
|
"import { createTopoloWidgetResponse } from '@topolo/sdk';",
|
|
4962
5240
|
"import { verifyRequest } from '@topolo-io/auth-middleware';",
|
|
@@ -4995,7 +5273,7 @@ function platformWorkerTs(application) {
|
|
|
4995
5273
|
" {",
|
|
4996
5274
|
" type: 'quick-actions',",
|
|
4997
5275
|
" actions: [",
|
|
4998
|
-
|
|
5276
|
+
` { id: 'open-workspace', label: '${escapeSingleQuoted(openActionLabel)}', link: '/', primary: true },`,
|
|
4999
5277
|
" { id: 'view-health', label: 'View health', link: '/api/health' },",
|
|
5000
5278
|
" ],",
|
|
5001
5279
|
" },",
|
|
@@ -5004,10 +5282,7 @@ function platformWorkerTs(application) {
|
|
|
5004
5282
|
" }",
|
|
5005
5283
|
"",
|
|
5006
5284
|
" if (url.pathname === '/api/bootstrap') {",
|
|
5007
|
-
|
|
5008
|
-
" user: auth.user,",
|
|
5009
|
-
" organization: auth.organization,",
|
|
5010
|
-
" }, { status: 200 }, request);",
|
|
5285
|
+
...bootstrapBody,
|
|
5011
5286
|
" }",
|
|
5012
5287
|
"",
|
|
5013
5288
|
" return json({ error: 'not_found' }, { status: 404 }, request);",
|
|
@@ -5102,6 +5377,7 @@ function developerWorkerTs(application) {
|
|
|
5102
5377
|
function cloudcontrolJson(application, options) {
|
|
5103
5378
|
const slug = toSlug(application.id);
|
|
5104
5379
|
const host = application.productionUrl ? new URL(application.productionUrl).host : `${slug}.topolo.app`;
|
|
5380
|
+
const stagingHost = `${slug}.stg.topolo.us`;
|
|
5105
5381
|
const deployTargets = options.api ? [
|
|
5106
5382
|
{
|
|
5107
5383
|
kind: "worker",
|
|
@@ -5117,6 +5393,21 @@ function cloudcontrolJson(application, options) {
|
|
|
5117
5393
|
routes: [host],
|
|
5118
5394
|
api_url: application.productionUrl,
|
|
5119
5395
|
service_id: application.serviceId
|
|
5396
|
+
},
|
|
5397
|
+
{
|
|
5398
|
+
kind: "worker",
|
|
5399
|
+
environment: "staging",
|
|
5400
|
+
target_name: `topolo-${slug}-staging`,
|
|
5401
|
+
cloudflare_account_id: STAGING_CLOUDFLARE_ACCOUNT_ID,
|
|
5402
|
+
config_path: "wrangler.toml",
|
|
5403
|
+
build_command: "VITE_TOPOLO_AUTH_URL=https://auth.stg.topolo.us npm run build",
|
|
5404
|
+
output_dir: "dist",
|
|
5405
|
+
production_branch: "staging",
|
|
5406
|
+
healthcheck_path: "/api/health",
|
|
5407
|
+
domains: [stagingHost],
|
|
5408
|
+
routes: [stagingHost],
|
|
5409
|
+
api_url: `https://${stagingHost}`,
|
|
5410
|
+
service_id: application.serviceId
|
|
5120
5411
|
}
|
|
5121
5412
|
] : [
|
|
5122
5413
|
{
|
|
@@ -5152,6 +5443,12 @@ function cloudcontrolJson(application, options) {
|
|
|
5152
5443
|
url: application.productionUrl,
|
|
5153
5444
|
branch: "main",
|
|
5154
5445
|
status: "unknown"
|
|
5446
|
+
},
|
|
5447
|
+
{
|
|
5448
|
+
name: "staging",
|
|
5449
|
+
url: `https://${stagingHost}`,
|
|
5450
|
+
branch: "staging",
|
|
5451
|
+
status: "unknown"
|
|
5155
5452
|
}
|
|
5156
5453
|
] : [],
|
|
5157
5454
|
deploy_targets: deployTargets,
|
|
@@ -5175,6 +5472,9 @@ function systemJson(application, options) {
|
|
|
5175
5472
|
const slug = toSlug(application.id);
|
|
5176
5473
|
const suiteLabel = options.audience === "personal" ? "personal" : "platform";
|
|
5177
5474
|
const summaryLabel = options.audience === "personal" ? "first-party Topolo personal application" : "first-party Topolo platform application";
|
|
5475
|
+
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.";
|
|
5476
|
+
const sensitiveData = options.tenancy === "household" ? ["identity", "personal_data", "household_data"] : ["identity", "org_data"];
|
|
5477
|
+
const privacySensitiveData = options.tenancy === "household" ? ["identity", "personal", "household"] : ["identity", "organization"];
|
|
5178
5478
|
return `${json({
|
|
5179
5479
|
id: `topolo-${slug}`,
|
|
5180
5480
|
name: toTitle(application.name),
|
|
@@ -5190,7 +5490,7 @@ function systemJson(application, options) {
|
|
|
5190
5490
|
api_contract: {
|
|
5191
5491
|
type: options.api ? "curated" : "none",
|
|
5192
5492
|
source: options.api ? `PlatformApplications/${application.appDir}/src/worker.ts` : `PlatformApplications/${application.appDir}/src/App.tsx`,
|
|
5193
|
-
notes:
|
|
5493
|
+
notes: apiNotes
|
|
5194
5494
|
},
|
|
5195
5495
|
primary_hosts: application.productionUrl ? [application.productionUrl] : [],
|
|
5196
5496
|
doc_paths: [`internal/apps/${slug}`, `public/applications/${slug}`],
|
|
@@ -5199,12 +5499,26 @@ function systemJson(application, options) {
|
|
|
5199
5499
|
auth_boundary: "Topolo Auth through @topolo-io/auth-client for browser sessions and @topolo-io/auth-middleware for Worker requests.",
|
|
5200
5500
|
tenant_isolation: options.tenancy === "household" ? "household_scoped" : "organization_scoped",
|
|
5201
5501
|
external_inputs: ["browser", ...options.api ? ["api"] : []],
|
|
5202
|
-
sensitive_data:
|
|
5502
|
+
sensitive_data: sensitiveData,
|
|
5203
5503
|
security_review_status: "not_started",
|
|
5204
5504
|
last_security_review: CURRENT_VERIFICATION_DATE,
|
|
5205
5505
|
pentest_status: "not_started",
|
|
5206
5506
|
evidence_doc: `internal/apps/${slug}`
|
|
5207
5507
|
},
|
|
5508
|
+
data_privacy: {
|
|
5509
|
+
classification: "confidential",
|
|
5510
|
+
sensitive_data_classes: privacySensitiveData,
|
|
5511
|
+
storage_locations: ["source_control"],
|
|
5512
|
+
encryption_at_rest: "platform_managed",
|
|
5513
|
+
encryption_in_transit: "https_only",
|
|
5514
|
+
key_management: "cloudflare_managed",
|
|
5515
|
+
retention_policy: "needs_policy",
|
|
5516
|
+
deletion_export_status: "planned",
|
|
5517
|
+
logging_redaction_status: "partial",
|
|
5518
|
+
privacy_review_status: "not_started",
|
|
5519
|
+
enterprise_ready: false,
|
|
5520
|
+
evidence_doc: `internal/apps/${slug}`
|
|
5521
|
+
},
|
|
5208
5522
|
dependencies: options.audience === "personal" ? ["topolo-auth"] : ["topolo-auth", "topolo-platform"],
|
|
5209
5523
|
tags: ["application", "cloudflare", "typescript", suiteLabel]
|
|
5210
5524
|
})}
|
|
@@ -5215,6 +5529,8 @@ function internalDocs(application, options) {
|
|
|
5215
5529
|
const title = toTitle(application.name);
|
|
5216
5530
|
const suiteLabel = options.audience === "personal" ? "personal" : "platform";
|
|
5217
5531
|
const tenantLabel = options.tenancy === "household" ? "household" : "organization";
|
|
5532
|
+
const apiContextLabel = options.tenancy === "household" ? "active personal context and selected household context" : "organization context";
|
|
5533
|
+
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
5534
|
return [
|
|
5219
5535
|
"---",
|
|
5220
5536
|
`title: "${escapeFrontmatter(title)} Handbook"`,
|
|
@@ -5257,11 +5573,11 @@ function internalDocs(application, options) {
|
|
|
5257
5573
|
"",
|
|
5258
5574
|
"## API Reference",
|
|
5259
5575
|
"",
|
|
5260
|
-
options.api ?
|
|
5576
|
+
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
5577
|
"",
|
|
5262
5578
|
"## Auth and Permissions",
|
|
5263
5579
|
"",
|
|
5264
|
-
options.api ? `Browser sessions and Worker requests must be validated through Topolo Auth under service id \`${application.serviceId}\`. Worker routes derive
|
|
5580
|
+
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
5581
|
"",
|
|
5266
5582
|
"## Data Ownership",
|
|
5267
5583
|
"",
|
|
@@ -5269,14 +5585,14 @@ function internalDocs(application, options) {
|
|
|
5269
5585
|
"",
|
|
5270
5586
|
"## Deployments",
|
|
5271
5587
|
"",
|
|
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.",
|
|
5588
|
+
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
5589
|
"",
|
|
5274
5590
|
"## Failure Modes",
|
|
5275
5591
|
"",
|
|
5276
5592
|
"- Auth service registration drifts from the generated Worker `SERVICE_ID`.",
|
|
5277
5593
|
"- CloudControl metadata does not match the eventual Cloudflare project or route.",
|
|
5278
5594
|
"- linked Topolo packages resolve a second React runtime and the browser shell renders blank instead of mounting the shared surfaces.",
|
|
5279
|
-
|
|
5595
|
+
`- ${boundaryFailureText}`,
|
|
5280
5596
|
"",
|
|
5281
5597
|
"## Debugging",
|
|
5282
5598
|
"",
|
|
@@ -5299,6 +5615,8 @@ function publicDocs(application, options) {
|
|
|
5299
5615
|
const title = toTitle(application.name);
|
|
5300
5616
|
const suiteLabel = options.audience === "personal" ? "personal" : "business";
|
|
5301
5617
|
const tenantLabel = options.tenancy === "household" ? "household" : "organization";
|
|
5618
|
+
const apiRouteLabel = options.tenancy === "household" ? "authenticated personal and household routes" : "authenticated workspace routes";
|
|
5619
|
+
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
5620
|
return [
|
|
5303
5621
|
"---",
|
|
5304
5622
|
`title: "${escapeFrontmatter(title)}"`,
|
|
@@ -5334,7 +5652,7 @@ function publicDocs(application, options) {
|
|
|
5334
5652
|
"",
|
|
5335
5653
|
"## API Reference",
|
|
5336
5654
|
"",
|
|
5337
|
-
options.api ?
|
|
5655
|
+
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
5656
|
"",
|
|
5339
5657
|
"## Auth and Permissions",
|
|
5340
5658
|
"",
|
|
@@ -5346,12 +5664,12 @@ function publicDocs(application, options) {
|
|
|
5346
5664
|
"",
|
|
5347
5665
|
"## Deployments",
|
|
5348
5666
|
"",
|
|
5349
|
-
application.productionUrl ? `The planned production host is \`${application.productionUrl}\`.` : "The production host has not been assigned yet.",
|
|
5667
|
+
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
5668
|
"",
|
|
5351
5669
|
"## Failure Modes",
|
|
5352
5670
|
"",
|
|
5353
5671
|
"- service registration and app routing can drift while the product is still planned",
|
|
5354
|
-
|
|
5672
|
+
`- ${dataModelFailure}`,
|
|
5355
5673
|
"",
|
|
5356
5674
|
"## Debugging",
|
|
5357
5675
|
"",
|
|
@@ -5360,7 +5678,7 @@ function publicDocs(application, options) {
|
|
|
5360
5678
|
"",
|
|
5361
5679
|
"## Change Log / Verification",
|
|
5362
5680
|
"",
|
|
5363
|
-
`- Scaffold generated on ${CURRENT_VERIFICATION_DATE} from
|
|
5681
|
+
`- Scaffold generated on ${CURRENT_VERIFICATION_DATE} from \`${options.portfolio ? "topolo platform apps scaffold" : "topolo apps scaffold"}\`.`,
|
|
5364
5682
|
""
|
|
5365
5683
|
].join("\n");
|
|
5366
5684
|
}
|
|
@@ -5369,8 +5687,17 @@ function serviceManifestJson(application, options) {
|
|
|
5369
5687
|
const title = toTitle(application.name);
|
|
5370
5688
|
const permissionPrefix = application.id.replace(/[^a-z0-9_]/g, "_");
|
|
5371
5689
|
const resourceLabel = options.tenancy === "household" ? "household" : "workspace";
|
|
5690
|
+
const launcherSettings = options.tenancy === "household" ? {
|
|
5691
|
+
settings: {
|
|
5692
|
+
launcher: {
|
|
5693
|
+
supported_contexts: ["personal"],
|
|
5694
|
+
household_capable: true
|
|
5695
|
+
}
|
|
5696
|
+
}
|
|
5697
|
+
} : {};
|
|
5372
5698
|
return `${json({
|
|
5373
5699
|
serviceId: application.serviceId,
|
|
5700
|
+
...launcherSettings,
|
|
5374
5701
|
systems: [
|
|
5375
5702
|
{
|
|
5376
5703
|
systemId: `topolo-${slug}`,
|
|
@@ -5569,7 +5896,6 @@ function escapeFrontmatter(value) {
|
|
|
5569
5896
|
}
|
|
5570
5897
|
|
|
5571
5898
|
// src/commands/app-scaffold-provision.ts
|
|
5572
|
-
var DEFAULT_DEVELOPERS_ORIGIN = "https://developers.topolo.app";
|
|
5573
5899
|
var DEVELOPERS_SERVICE_ID = "svc_topolo_developers";
|
|
5574
5900
|
async function provisionApplicationScaffold(payload) {
|
|
5575
5901
|
if (!payload.application.productionUrl) {
|
|
@@ -5582,7 +5908,8 @@ async function provisionApplicationScaffold(payload) {
|
|
|
5582
5908
|
"Platform scaffold provisioning requires a service id. Re-run without --no-api or pass --service."
|
|
5583
5909
|
);
|
|
5584
5910
|
}
|
|
5585
|
-
const
|
|
5911
|
+
const environment = selectedEnvironment();
|
|
5912
|
+
const credential = await refreshCredentialIfNeeded(environment);
|
|
5586
5913
|
if (!credential) {
|
|
5587
5914
|
throw new Error(
|
|
5588
5915
|
"topolo apps scaffold --provision requires an authenticated Topolo session. Run `topolo auth login` first."
|
|
@@ -5596,15 +5923,16 @@ async function provisionApplicationScaffold(payload) {
|
|
|
5596
5923
|
}
|
|
5597
5924
|
const identity = await client.client.introspect();
|
|
5598
5925
|
const config = await readConfig();
|
|
5599
|
-
const
|
|
5600
|
-
const developersOrigin = sanitizeOrigin(serviceUrls
|
|
5926
|
+
const profile = resolvedProfileConfig(config, environment);
|
|
5927
|
+
const developersOrigin = sanitizeOrigin(profile.serviceUrls?.developers ?? "https://developers.topolo.app");
|
|
5928
|
+
assertServiceUrlForEnvironment("developers", developersOrigin, environment);
|
|
5601
5929
|
const endpoint = payload.target === "platform" ? "/api/developer-console/admin/first-party-app-scaffolds" : "/api/developer-console/apps/scaffold";
|
|
5602
5930
|
const headers = new Headers({
|
|
5603
5931
|
"Content-Type": "application/json",
|
|
5604
5932
|
"X-Service-ID": DEVELOPERS_SERVICE_ID,
|
|
5605
5933
|
"X-Topolo-Client": `topolo-cli/${CLI_VERSION}`
|
|
5606
5934
|
});
|
|
5607
|
-
const agentName = resolveAgentName(
|
|
5935
|
+
const agentName = resolveAgentName(profile.agentName);
|
|
5608
5936
|
if (agentName) {
|
|
5609
5937
|
headers.set("X-Topolo-Agent", agentName);
|
|
5610
5938
|
}
|
|
@@ -6361,39 +6689,48 @@ function registerServicesCommand(program2) {
|
|
|
6361
6689
|
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
6690
|
const mode = resolveOutputMode(opts.json);
|
|
6363
6691
|
try {
|
|
6692
|
+
const environment = selectedEnvironment();
|
|
6364
6693
|
const config = await readConfig();
|
|
6365
|
-
const rows = buildRows(
|
|
6366
|
-
emit({ services: rows }, mode, () => formatRows2(rows));
|
|
6694
|
+
const rows = buildRows(config.serviceUrls, environment);
|
|
6695
|
+
emit({ environment, services: rows }, mode, () => formatRows2(rows, environment));
|
|
6367
6696
|
} catch (err) {
|
|
6368
6697
|
fail(err, mode);
|
|
6369
6698
|
}
|
|
6370
6699
|
});
|
|
6371
6700
|
}
|
|
6372
|
-
function buildRows(overrides) {
|
|
6701
|
+
function buildRows(overrides, environment = selectedEnvironment()) {
|
|
6702
|
+
const resolvedOverrides = resolveServiceUrls(overrides, environment);
|
|
6373
6703
|
return Object.keys(DEFAULT_SERVICE_URLS).sort().map((id) => {
|
|
6374
6704
|
const defaultUrl = DEFAULT_SERVICE_URLS[id];
|
|
6375
|
-
const effectiveUrl = resolveServiceUrl(id,
|
|
6705
|
+
const effectiveUrl = resolveServiceUrl(id, resolvedOverrides);
|
|
6376
6706
|
return {
|
|
6377
6707
|
id,
|
|
6378
6708
|
defaultUrl,
|
|
6379
6709
|
effectiveUrl,
|
|
6380
|
-
overridden: defaultUrl !== effectiveUrl
|
|
6710
|
+
overridden: defaultUrl !== effectiveUrl,
|
|
6711
|
+
environmentSafe: environment !== "staging" || isStagingUrl(effectiveUrl)
|
|
6381
6712
|
};
|
|
6382
6713
|
});
|
|
6383
6714
|
}
|
|
6384
|
-
function formatRows2(rows) {
|
|
6715
|
+
function formatRows2(rows, environment = selectedEnvironment()) {
|
|
6385
6716
|
const idWidth = Math.max(3, ...rows.map((r) => r.id.length));
|
|
6386
6717
|
const urlWidth = Math.max(7, ...rows.map((r) => r.effectiveUrl.length));
|
|
6387
6718
|
const lines = [];
|
|
6388
|
-
lines.push(
|
|
6389
|
-
lines.push(
|
|
6719
|
+
lines.push(`Environment: ${environment}`);
|
|
6720
|
+
lines.push("");
|
|
6721
|
+
lines.push(`${"ID".padEnd(idWidth)} ${"URL".padEnd(urlWidth)} OVERRIDE READY`);
|
|
6722
|
+
lines.push(`${"-".repeat(idWidth)} ${"-".repeat(urlWidth)} -------- -----`);
|
|
6390
6723
|
for (const r of rows) {
|
|
6391
6724
|
lines.push(
|
|
6392
|
-
`${r.id.padEnd(idWidth)} ${r.effectiveUrl.padEnd(urlWidth)} ${r.overridden ? "yes" : ""}`
|
|
6725
|
+
`${r.id.padEnd(idWidth)} ${r.effectiveUrl.padEnd(urlWidth)} ${r.overridden ? "yes" : "".padEnd(8)} ${r.environmentSafe ? "yes" : "no"}`
|
|
6393
6726
|
);
|
|
6394
6727
|
}
|
|
6395
6728
|
return lines.join("\n");
|
|
6396
6729
|
}
|
|
6730
|
+
function isStagingUrl(value) {
|
|
6731
|
+
const url = value.toLowerCase();
|
|
6732
|
+
return url.includes(".stg.topolo.us") || url.includes("localhost") || url.includes("127.0.0.1") || url.includes("topolo-staging");
|
|
6733
|
+
}
|
|
6397
6734
|
|
|
6398
6735
|
// src/commands/seed.ts
|
|
6399
6736
|
var DEFAULT_SEED_BASE_URL = "https://seed.stg.topolo.us";
|
|
@@ -6401,9 +6738,9 @@ function seedBaseUrl() {
|
|
|
6401
6738
|
return process.env.TOPOLO_SEED_URL || DEFAULT_SEED_BASE_URL;
|
|
6402
6739
|
}
|
|
6403
6740
|
async function authedFetch(path4, init = {}) {
|
|
6404
|
-
const credential = await refreshCredentialIfNeeded();
|
|
6741
|
+
const credential = await refreshCredentialIfNeeded("staging");
|
|
6405
6742
|
if (!credential) {
|
|
6406
|
-
throw new Error("No credential configured. Run `topolo auth login` or set
|
|
6743
|
+
throw new Error("No staging credential configured. Run `topolo auth login --env staging` or set TOPOLO_STAGING_API_KEY.");
|
|
6407
6744
|
}
|
|
6408
6745
|
const headers = new Headers(init.headers || {});
|
|
6409
6746
|
if (credential.kind === "api_key") headers.set("X-Api-Key", credential.apiKey);
|
|
@@ -6611,9 +6948,22 @@ function formatManifestsTable(manifests) {
|
|
|
6611
6948
|
|
|
6612
6949
|
// src/index.ts
|
|
6613
6950
|
var program = new Command();
|
|
6951
|
+
var argv;
|
|
6952
|
+
try {
|
|
6953
|
+
argv = consumeEnvironmentArg(process.argv);
|
|
6954
|
+
} catch (err) {
|
|
6955
|
+
process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}
|
|
6956
|
+
`);
|
|
6957
|
+
process.exit(1);
|
|
6958
|
+
}
|
|
6614
6959
|
program.name("topolo").description(
|
|
6615
6960
|
"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);
|
|
6961
|
+
).version(CLI_VERSION).option("--env <environment>", "Topolo environment: production or staging");
|
|
6962
|
+
program.hook("preAction", (thisCommand) => {
|
|
6963
|
+
const opts = thisCommand.opts();
|
|
6964
|
+
if (!opts.env) return;
|
|
6965
|
+
process.env.TOPOLO_ENV = normalizeEnvironment(opts.env);
|
|
6966
|
+
});
|
|
6617
6967
|
registerAuthCommands(program);
|
|
6618
6968
|
registerWhoamiCommand(program);
|
|
6619
6969
|
registerCrmCommands(program);
|
|
@@ -6622,8 +6972,30 @@ registerAppsCommand(program);
|
|
|
6622
6972
|
registerPlatformCommand(program);
|
|
6623
6973
|
registerServicesCommand(program);
|
|
6624
6974
|
registerSeedCommands(program);
|
|
6625
|
-
program.parseAsync(
|
|
6975
|
+
program.parseAsync(argv).catch((err) => {
|
|
6626
6976
|
process.stderr.write(`Unexpected error: ${err instanceof Error ? err.message : String(err)}
|
|
6627
6977
|
`);
|
|
6628
6978
|
process.exit(1);
|
|
6629
6979
|
});
|
|
6980
|
+
function consumeEnvironmentArg(argv2) {
|
|
6981
|
+
const next = argv2.slice(0, 2);
|
|
6982
|
+
for (let i = 2; i < argv2.length; i += 1) {
|
|
6983
|
+
const arg = argv2[i];
|
|
6984
|
+
if (!arg) continue;
|
|
6985
|
+
if (arg === "--env") {
|
|
6986
|
+
const value = argv2[i + 1];
|
|
6987
|
+
if (!value || value.startsWith("-")) {
|
|
6988
|
+
throw new Error("--env requires production or staging.");
|
|
6989
|
+
}
|
|
6990
|
+
process.env.TOPOLO_ENV = normalizeEnvironment(value);
|
|
6991
|
+
i += 1;
|
|
6992
|
+
continue;
|
|
6993
|
+
}
|
|
6994
|
+
if (arg.startsWith("--env=")) {
|
|
6995
|
+
process.env.TOPOLO_ENV = normalizeEnvironment(arg.slice("--env=".length));
|
|
6996
|
+
continue;
|
|
6997
|
+
}
|
|
6998
|
+
next.push(arg);
|
|
6999
|
+
}
|
|
7000
|
+
return next;
|
|
7001
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@topolo/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
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": {
|