@nurix/apollo 0.4.0 → 0.4.2

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 CHANGED
@@ -1,19 +1,12 @@
1
1
  # `@nurix/apollo` — the `apollo` CLI
2
2
 
3
- The consumer-side installer for the Apollo discovery plane. It implements the
4
- build-time integration flow of the
5
- [consumer journey](../../docs/discovery/consumer-journey.md): exchange your
6
- application key for service-scoped credentials, write them into a managed
7
- `.env` block, install the service's npm package, and record the `consumes`
8
- edge in your `apollo.service.json` (the application's bill of materials).
3
+ The consumer-side installer for the Apollo discovery plane. It implements the build-time integration flow of the [consumer journey](../../docs/discovery/consumer-journey.md): exchange your application key for service-scoped credentials, write them into a managed `.env` block, install the service's npm package, and record the `consumes` edge in your `apollo.service.json` (the application's bill of materials).
9
4
 
10
5
  Interactive UX is built on [`@clack/prompts`](https://github.com/bombshell-dev/clack).
11
6
 
12
7
  ## Install
13
8
 
14
- Published to npm with **restricted access** under the `@nurix` scope — you need
15
- an npm account with read rights to the `nurix` org (`npm login`, then verify
16
- with `npm whoami`).
9
+ Published to npm with **restricted access** under the `@nurix` scope — you need an npm account with read rights to the `nurix` org (`npm login`, then verify with `npm whoami`).
17
10
 
18
11
  ```
19
12
  npx @nurix/apollo@latest --help # one-shot
@@ -53,37 +46,27 @@ apollo doctor # key validity + per-credential health probes +
53
46
  apollo open <svc> [--env <name>] # open endpoint (default production) or repo URL
54
47
  ```
55
48
 
56
- The Apollo base URL is hardcoded to the hosted instance
57
- (`https://apollo.nurix-ai.co`) — there is no flag or env override.
49
+ Global options: `--apollo-url <url>` (defaults to `$APOLLO_URL`, then the hosted instance `https://apollo.nurix-ai.co`) — the default means you never need to pass the URL; override only for a non-hosted Apollo.
58
50
 
59
51
  ### `apollo init`
60
52
 
61
- 1. Prompts for your `APOLLO_APP_KEY` (created with your application in the
62
- Apollo console) and **validates it against Apollo no key, no install**.
63
- 2. Writes it into the **Apollo-managed block** of `.env` (+ a placeholder in
64
- `.env.example`) and makes sure `.env` is gitignored.
53
+ 1. Prompts for your `APOLLO_APP_KEY` (created with your application in the Apollo console) and **validates it against Apollo — no key, no install**.
54
+ 2. Writes it into the **Apollo-managed block** of `.env` (+ a placeholder in `.env.example`) and makes sure `.env` is gitignored.
65
55
  3. Offers to scaffold `apollo.service.json` if the repo has none.
66
56
 
67
57
  ### `apollo add <service>` — the installer
68
58
 
69
- 1. Reads `APOLLO_APP_KEY` from the managed block (or the environment); fails
70
- loudly with remediation if missing.
71
- 2. Fetches the catalogue and resolves the service (interactive select when no
72
- name is passed).
73
- 3. `POST /api/v1/keys/exchange` `{ endpoint, serviceKey }` idempotent per
74
- (application × service), so re-running is safe.
75
- 4. Writes `<SERVICE>_ENDPOINT` + `<SERVICE>_KEY` into the managed `.env`
76
- block, placeholders into `.env.example`.
77
- 5. Installs the service's npm package when it ships one (package manager
78
- detected from the repo's lockfile).
79
- 6. Appends the `consumes` edge to `apollo.service.json` (offers a scaffold if
80
- the manifest is missing).
59
+ 1. Reads `APOLLO_APP_KEY` from the managed block (or the environment); fails loudly with remediation if missing.
60
+ 2. Fetches the catalogue and resolves the service (interactive select when no name is passed).
61
+ 3. `POST /api/v1/keys/exchange` `{ endpoint, serviceKey }` — idempotent per (application × service), so re-running is safe.
62
+ 4. Writes `<SERVICE>_ENDPOINT` + `<SERVICE>_KEY` into the managed `.env` block, placeholders into `.env.example`.
63
+ 5. Installs the service's npm package when it ships one (package manager detected from the repo's lockfile).
64
+ 6. Appends the `consumes` edge to `apollo.service.json` (offers a scaffold if the manifest is missing).
81
65
  7. Best-effort health probe against the issued endpoint (never blocks).
82
66
 
83
67
  ### The managed `.env` block
84
68
 
85
- The CLI owns **only** the delimited block — everything outside it is yours and
86
- is never touched:
69
+ The CLI owns **only** the delimited block — everything outside it is yours and is never touched:
87
70
 
88
71
  ```
89
72
  MY_OWN_SECRET=untouched
@@ -95,8 +78,7 @@ EMAIL_KEY=apollo_svc_…
95
78
  # <<< apollo:managed <<<
96
79
  ```
97
80
 
98
- Inside the block the server is the source of truth (manual edits get
99
- overwritten); outside it, nothing is ever rewritten.
81
+ Inside the block the server is the source of truth (manual edits get overwritten); outside it, nothing is ever rewritten.
100
82
 
101
83
  ## Development
102
84
 
@@ -106,20 +88,10 @@ pnpm --filter @nurix/apollo typecheck
106
88
  node packages/cli/dist/index.js --help # run from the repo root
107
89
  ```
108
90
 
109
- ESM, Node ≥ 22, strict TypeScript. `bin` is `dist/index.js` (`apollo`).
110
- Releases ship via `.github/workflows/publish-cli-to-npm.yml` — merges to `dev`
111
- touching `packages/cli/**` auto-publish a patch; dispatch the workflow for
112
- minor/major.
91
+ ESM, Node ≥ 22, strict TypeScript. `bin` is `dist/index.js` (`apollo`). Releases ship via `.github/workflows/publish-cli-to-npm.yml` — merges to `dev` touching `packages/cli/**` auto-publish a patch; dispatch the workflow for minor/major.
113
92
 
114
93
  ## Current limitations
115
94
 
116
- - The hosted catalogue is empty until the registry read path lands
117
- (consumer-journey.md R3) — `list`/`describe`/`graph`/`open` degrade with a
118
- clear message; `apollo add <name>` still works for services seeded into the
119
- KV catalogue, since the exchange resolves endpoints server-side.
120
- - `apollo sync` needs `GET /api/v1/keys/credentials` (journey R7) — the route
121
- is in the worker code; it goes live with the next worker deploy.
122
- - Exchange still returns `{endpoint, serviceKey}` together; once it slims to
123
- credential-only with Apollo-served definitions (journey §9.2–§9.3), the
124
- client picks the endpoint up from the definition instead
125
- (`src/lib/apollo_client.ts` is the seam).
95
+ - The hosted catalogue is empty until the registry read path lands (consumer-journey.md R3) — `list`/`describe`/`graph`/`open` degrade with a clear message; `apollo add <name>` still works for services seeded into the KV catalogue, since the exchange resolves endpoints server-side.
96
+ - `apollo sync` needs `GET /api/v1/keys/credentials` (journey R7) — the route is in the worker code; it goes live with the next worker deploy.
97
+ - Exchange still returns `{endpoint, serviceKey}` together; once it slims to credential-only with Apollo-served definitions (journey §9.2–§9.3), the client picks the endpoint up from the definition instead (`src/lib/apollo_client.ts` is the seam).
@@ -4,14 +4,15 @@
4
4
  // records the consumes edge (consumer-journey.md §4–§5).
5
5
  import path from "node:path";
6
6
  import * as p from "@clack/prompts";
7
- import { APOLLO_URL, exchangeServiceCredential, fetchCatalogue, } from "../lib/apollo_client.js";
7
+ import { exchangeServiceCredential, fetchCatalogue, resolveApolloUrl, } from "../lib/apollo_client.js";
8
8
  import { readManagedValues, upsertManagedValues } from "../lib/env_file.js";
9
9
  import { detectPackageManager, installPackage } from "../lib/package_manager.js";
10
10
  import { appendConsumesEdge, hasManifest, scaffoldManifest } from "../lib/manifest.js";
11
11
  import { confirmOrFallback, isInteractive } from "../lib/interactive.js";
12
12
  // Run the installer flow end to end for one service.
13
- export async function runAdd(serviceArg) {
13
+ export async function runAdd(serviceArg, options) {
14
14
  const cwd = process.cwd();
15
+ const baseUrl = resolveApolloUrl(options);
15
16
  p.intro(`apollo add${serviceArg ? ` ${serviceArg}` : ""}`);
16
17
  const managedValues = await readManagedValues(path.join(cwd, ".env"));
17
18
  const appKey = managedValues.APOLLO_APP_KEY ?? process.env.APOLLO_APP_KEY;
@@ -23,7 +24,7 @@ export async function runAdd(serviceArg) {
23
24
  }
24
25
  const spinner = p.spinner();
25
26
  spinner.start("Fetching the catalogue");
26
- const catalogue = await fetchCatalogue(APOLLO_URL).catch(() => null);
27
+ const catalogue = await fetchCatalogue(baseUrl).catch(() => null);
27
28
  const services = catalogue?.services ?? [];
28
29
  spinner.stop(`Catalogue loaded: ${services.length} service(s)`);
29
30
  const serviceName = serviceArg ?? (await pickService(services));
@@ -39,7 +40,7 @@ export async function runAdd(serviceArg) {
39
40
  spinner.start(`Exchanging credentials for '${serviceName}'`);
40
41
  let credential;
41
42
  try {
42
- credential = await exchangeServiceCredential(APOLLO_URL, appKey, serviceName);
43
+ credential = await exchangeServiceCredential(baseUrl, appKey, serviceName);
43
44
  }
44
45
  catch (error) {
45
46
  spinner.stop("Exchange failed", 1);
@@ -3,14 +3,15 @@
3
3
  // rejected, runs the login flow, then finishes with init's manifest scaffold.
4
4
  import path from "node:path";
5
5
  import * as p from "@clack/prompts";
6
- import { APOLLO_URL, validateAppKey } from "../lib/apollo_client.js";
6
+ import { resolveApolloUrl, validateAppKey } from "../lib/apollo_client.js";
7
7
  import { readManagedValues } from "../lib/env_file.js";
8
8
  import { isInteractive } from "../lib/interactive.js";
9
9
  import { performDeviceLogin } from "./login.js";
10
10
  import { finishInit } from "./init.js";
11
11
  // Run the bootstrap flow: ensure a valid app key (login when needed), then init.
12
- export async function runBootstrap() {
12
+ export async function runBootstrap(options) {
13
13
  const cwd = process.cwd();
14
+ const baseUrl = resolveApolloUrl(options);
14
15
  p.intro("apollo");
15
16
  if (!isInteractive()) {
16
17
  p.log.error("Bare `apollo` needs an interactive terminal (browser sign-in + prompts).");
@@ -23,7 +24,7 @@ export async function runBootstrap() {
23
24
  if (managedValues.APOLLO_APP_KEY) {
24
25
  const spinner = p.spinner();
25
26
  spinner.start("Checking the stored APOLLO_APP_KEY against Apollo");
26
- isSignedIn = await validateAppKey(APOLLO_URL, managedValues.APOLLO_APP_KEY).catch(() => false);
27
+ isSignedIn = await validateAppKey(baseUrl, managedValues.APOLLO_APP_KEY).catch(() => false);
27
28
  spinner.stop(isSignedIn
28
29
  ? "Signed in — the stored key is valid"
29
30
  : "Stored key rejected — signing in again", isSignedIn ? 0 : 1);
@@ -31,7 +32,7 @@ export async function runBootstrap() {
31
32
  else {
32
33
  p.log.info("No APOLLO_APP_KEY in the managed .env block — signing in");
33
34
  }
34
- if (!isSignedIn && !(await performDeviceLogin(cwd))) {
35
+ if (!isSignedIn && !(await performDeviceLogin(baseUrl, cwd))) {
35
36
  p.outro("Sign-in failed — nothing was written. Run `apollo` again, or `apollo login` to retry.");
36
37
  process.exitCode = 1;
37
38
  return;
@@ -1,13 +1,14 @@
1
1
  // packages/cli/src/commands/describe.ts - `apollo describe <service>`: the full
2
2
  // catalogue entry for one service, prettified or raw JSON.
3
3
  import * as p from "@clack/prompts";
4
- import { APOLLO_URL, fetchCatalogue } from "../lib/apollo_client.js";
4
+ import { fetchCatalogue, resolveApolloUrl } from "../lib/apollo_client.js";
5
5
  // Print one catalogue entry; --json emits the raw object for piping.
6
- export async function runDescribe(serviceName, options) {
7
- const catalogue = await fetchCatalogue(APOLLO_URL);
6
+ export async function runDescribe(serviceName, options, globals) {
7
+ const baseUrl = resolveApolloUrl(globals);
8
+ const catalogue = await fetchCatalogue(baseUrl);
8
9
  const entry = (catalogue.services ?? []).find((service) => service.name === serviceName);
9
10
  if (!entry) {
10
- p.log.error(`'${serviceName}' is not in the catalogue at ${APOLLO_URL} (${catalogue.services?.length ?? 0} services listed).`);
11
+ p.log.error(`'${serviceName}' is not in the catalogue at ${baseUrl} (${catalogue.services?.length ?? 0} services listed).`);
11
12
  process.exitCode = 1;
12
13
  return;
13
14
  }
@@ -5,19 +5,20 @@
5
5
  import path from "node:path";
6
6
  import { readFile } from "node:fs/promises";
7
7
  import * as p from "@clack/prompts";
8
- import { APOLLO_URL, fetchCatalogue, validateAppKey } from "../lib/apollo_client.js";
8
+ import { fetchCatalogue, resolveApolloUrl, validateAppKey, } from "../lib/apollo_client.js";
9
9
  import { readManagedValues } from "../lib/env_file.js";
10
10
  import { hasManifest, manifestPath } from "../lib/manifest.js";
11
11
  import { loadSchema, validateManifest } from "../lib/schema_validator.js";
12
12
  // Run every check; exit 1 when any error (not warning) was found.
13
- export async function runDoctor() {
13
+ export async function runDoctor(globals) {
14
14
  const cwd = process.cwd();
15
+ const baseUrl = resolveApolloUrl(globals);
15
16
  p.intro("apollo doctor");
16
17
  const counters = { errors: 0, warnings: 0 };
17
18
  const managedValues = await readManagedValues(path.join(cwd, ".env"));
18
- await checkAppKey(managedValues, counters);
19
+ await checkAppKey(managedValues, baseUrl, counters);
19
20
  await checkServiceCredentials(managedValues, counters);
20
- await checkManifest(cwd, counters);
21
+ await checkManifest(cwd, baseUrl, counters);
21
22
  const summary = `${counters.errors} error(s), ${counters.warnings} warning(s)`;
22
23
  if (counters.errors > 0) {
23
24
  p.outro(`Unhealthy — ${summary}.`);
@@ -28,14 +29,14 @@ export async function runDoctor() {
28
29
  }
29
30
  }
30
31
  // Check the APOLLO_APP_KEY exists and is still accepted by Apollo.
31
- async function checkAppKey(managedValues, counters) {
32
+ async function checkAppKey(managedValues, baseUrl, counters) {
32
33
  const appKey = managedValues.APOLLO_APP_KEY ?? process.env.APOLLO_APP_KEY;
33
34
  if (!appKey) {
34
35
  p.log.error("APOLLO_APP_KEY missing — run `apollo init`.");
35
36
  counters.errors += 1;
36
37
  return;
37
38
  }
38
- const isValid = await validateAppKey(APOLLO_URL, appKey).catch(() => false);
39
+ const isValid = await validateAppKey(baseUrl, appKey).catch(() => false);
39
40
  if (isValid) {
40
41
  p.log.success("APOLLO_APP_KEY accepted by Apollo");
41
42
  }
@@ -80,7 +81,7 @@ async function checkServiceCredentials(managedValues, counters) {
80
81
  }
81
82
  }
82
83
  // Validate the manifest and cross-check its consumes edges against the catalogue.
83
- async function checkManifest(cwd, counters) {
84
+ async function checkManifest(cwd, baseUrl, counters) {
84
85
  if (!hasManifest(cwd)) {
85
86
  p.log.warn("No apollo.service.json — the app has no bill of materials. `apollo init` scaffolds one.");
86
87
  counters.warnings += 1;
@@ -109,7 +110,7 @@ async function checkManifest(cwd, counters) {
109
110
  const edges = (manifest.services ?? []).flatMap((service) => service.consumes ?? []);
110
111
  if (edges.length === 0)
111
112
  return;
112
- const catalogue = await fetchCatalogue(APOLLO_URL).catch(() => null);
113
+ const catalogue = await fetchCatalogue(baseUrl).catch(() => null);
113
114
  const services = catalogue?.services ?? [];
114
115
  if (services.length === 0) {
115
116
  p.log.info("Catalogue empty — consumes edges not cross-checked.");
@@ -1,9 +1,10 @@
1
1
  // packages/cli/src/commands/graph.ts - `apollo graph`: render the catalogue's
2
2
  // consumes-graph as an ascii edge list or a mermaid block.
3
- import { APOLLO_URL, fetchCatalogue } from "../lib/apollo_client.js";
3
+ import { fetchCatalogue, resolveApolloUrl } from "../lib/apollo_client.js";
4
4
  // Print every consumes edge in the catalogue; --mermaid emits `graph LR`.
5
- export async function runGraph(options) {
6
- const catalogue = await fetchCatalogue(APOLLO_URL);
5
+ export async function runGraph(options, globals) {
6
+ const baseUrl = resolveApolloUrl(globals);
7
+ const catalogue = await fetchCatalogue(baseUrl);
7
8
  const services = catalogue.services ?? [];
8
9
  const edges = services.flatMap((service) => (service.consumes ?? []).map((edge) => ({
9
10
  from: service.name,
@@ -11,7 +12,7 @@ export async function runGraph(options) {
11
12
  via: edge.via ?? "http",
12
13
  })));
13
14
  if (services.length === 0) {
14
- console.log(`Catalogue at ${APOLLO_URL} is empty — nothing to graph.`);
15
+ console.log(`Catalogue at ${baseUrl} is empty — nothing to graph.`);
15
16
  return;
16
17
  }
17
18
  if (edges.length === 0) {
@@ -3,14 +3,15 @@
3
3
  // gitignore .env, and scaffold the repo manifest.
4
4
  import path from "node:path";
5
5
  import * as p from "@clack/prompts";
6
- import { APOLLO_URL, validateAppKey } from "../lib/apollo_client.js";
6
+ import { resolveApolloUrl, validateAppKey } from "../lib/apollo_client.js";
7
7
  import { ensureEnvGitignored, readManagedValues, upsertManagedValues } from "../lib/env_file.js";
8
8
  import { hasManifest, scaffoldManifest } from "../lib/manifest.js";
9
9
  import { isInteractive } from "../lib/interactive.js";
10
10
  import { performDeviceLogin } from "./login.js";
11
11
  // Run the init flow: collect + validate the key, write managed blocks, scaffold.
12
- export async function runInit() {
12
+ export async function runInit(options) {
13
13
  const cwd = process.cwd();
14
+ const baseUrl = resolveApolloUrl(options);
14
15
  p.intro("apollo init");
15
16
  if (!isInteractive()) {
16
17
  p.log.error("apollo init needs an interactive terminal (it prompts for the key).");
@@ -32,7 +33,7 @@ export async function runInit() {
32
33
  message: "Sign in via the browser to create your application & key? (No = paste a key)",
33
34
  });
34
35
  if (!p.isCancel(viaBrowser) && viaBrowser) {
35
- if (!(await performDeviceLogin(cwd))) {
36
+ if (!(await performDeviceLogin(baseUrl, cwd))) {
36
37
  p.outro("Sign-in failed — re-run `apollo init`, or pick the paste option.");
37
38
  process.exitCode = 1;
38
39
  return;
@@ -41,7 +42,7 @@ export async function runInit() {
41
42
  return;
42
43
  }
43
44
  const appKey = await p.password({
44
- message: `Paste your APOLLO_APP_KEY (create an application in the Apollo console: ${APOLLO_URL})`,
45
+ message: `Paste your APOLLO_APP_KEY (create an application in the Apollo console: ${baseUrl})`,
45
46
  validate: (value) => value?.startsWith("apollo_app_") ? undefined : "Expected a key starting with apollo_app_",
46
47
  });
47
48
  if (p.isCancel(appKey)) {
@@ -50,7 +51,7 @@ export async function runInit() {
50
51
  }
51
52
  const spinner = p.spinner();
52
53
  spinner.start("Validating the key against Apollo");
53
- const isValid = await validateAppKey(APOLLO_URL, appKey).catch(() => false);
54
+ const isValid = await validateAppKey(baseUrl, appKey).catch(() => false);
54
55
  if (!isValid) {
55
56
  spinner.stop("Key rejected by Apollo", 1);
56
57
  p.outro("No key, no install — fix the key and re-run `apollo init`.");
@@ -1,12 +1,13 @@
1
1
  // packages/cli/src/commands/list.ts - `apollo list`: print the discovery
2
2
  // catalogue, one line per service.
3
- import { APOLLO_URL, fetchCatalogue } from "../lib/apollo_client.js";
3
+ import { fetchCatalogue, resolveApolloUrl } from "../lib/apollo_client.js";
4
4
  // Print the catalogue as aligned columns: name, classifier, status, description.
5
- export async function runList() {
6
- const catalogue = await fetchCatalogue(APOLLO_URL);
5
+ export async function runList(options) {
6
+ const baseUrl = resolveApolloUrl(options);
7
+ const catalogue = await fetchCatalogue(baseUrl);
7
8
  const services = catalogue.services ?? [];
8
9
  if (services.length === 0) {
9
- console.log(`Catalogue at ${APOLLO_URL} is empty — the registry read path / refresh isn't wired yet.`);
10
+ console.log(`Catalogue at ${baseUrl} is empty — the registry read path / refresh isn't wired yet.`);
10
11
  return;
11
12
  }
12
13
  for (const service of services) {
@@ -5,7 +5,7 @@
5
5
  // the managed .env block — no key pasting.
6
6
  import path from "node:path";
7
7
  import * as p from "@clack/prompts";
8
- import { APOLLO_URL } from "../lib/apollo_client.js";
8
+ import { resolveApolloUrl } from "../lib/apollo_client.js";
9
9
  import { ensureEnvGitignored, readManagedValues, upsertManagedValues } from "../lib/env_file.js";
10
10
  import { launchBrowser } from "../lib/browser_opener.js";
11
11
  import { getOrCreateDeviceId } from "../lib/device_identity.js";
@@ -13,9 +13,9 @@ import { pollDeviceOnce, pollUntilIssued, startDeviceTrust, } from "../lib/devic
13
13
  // Browser-approval wait cap; the trust window itself is 24h server-side.
14
14
  const APPROVAL_TIMEOUT_SECONDS = 10 * 60;
15
15
  // Run the login flow and write the issued key; returns false on failure.
16
- export async function performDeviceLogin(cwd, flags = {}) {
16
+ export async function performDeviceLogin(baseUrl, cwd, flags = {}) {
17
17
  const deviceId = await getOrCreateDeviceId();
18
- const start = await startDeviceTrust(APOLLO_URL, deviceId);
18
+ const start = await startDeviceTrust(baseUrl, deviceId);
19
19
  const managedValues = await readManagedValues(path.join(cwd, ".env"));
20
20
  const pollParams = {
21
21
  deviceCode: start.device_code,
@@ -26,7 +26,7 @@ export async function performDeviceLogin(cwd, flags = {}) {
26
26
  // Trusted-device fast path: an approval inside the 24h window is not
27
27
  // consumed, so a single poll may already succeed with no browser at all.
28
28
  let issued = null;
29
- const firstAttempt = await pollDeviceOnce(APOLLO_URL, pollParams);
29
+ const firstAttempt = await pollDeviceOnce(baseUrl, pollParams);
30
30
  if (firstAttempt.kind === "issued") {
31
31
  issued = firstAttempt.issued;
32
32
  p.log.success("Device already trusted — no browser approval needed");
@@ -43,7 +43,7 @@ export async function performDeviceLogin(cwd, flags = {}) {
43
43
  const spinner = p.spinner();
44
44
  spinner.start("Waiting for approval in the browser");
45
45
  try {
46
- issued = await pollUntilIssued(APOLLO_URL, pollParams, start.interval, Math.min(start.expires_in, APPROVAL_TIMEOUT_SECONDS));
46
+ issued = await pollUntilIssued(baseUrl, pollParams, start.interval, Math.min(start.expires_in, APPROVAL_TIMEOUT_SECONDS));
47
47
  }
48
48
  catch (error) {
49
49
  spinner.stop("Sign-in failed", 1);
@@ -68,10 +68,11 @@ export async function performDeviceLogin(cwd, flags = {}) {
68
68
  return true;
69
69
  }
70
70
  // `apollo login` — the standalone command wrapper.
71
- export async function runLogin(flags = {}) {
71
+ export async function runLogin(options, flags = {}) {
72
72
  const cwd = process.cwd();
73
+ const baseUrl = resolveApolloUrl(options);
73
74
  p.intro("apollo login");
74
- const didLogin = await performDeviceLogin(cwd, flags);
75
+ const didLogin = await performDeviceLogin(baseUrl, cwd, flags);
75
76
  if (!didLogin) {
76
77
  p.outro("Nothing was written.");
77
78
  process.exitCode = 1;
@@ -1,14 +1,15 @@
1
1
  // packages/cli/src/commands/open.ts - `apollo open <service>`: open the
2
2
  // service's endpoint (or repository) in the default browser.
3
3
  import * as p from "@clack/prompts";
4
- import { APOLLO_URL, fetchCatalogue } from "../lib/apollo_client.js";
4
+ import { fetchCatalogue, resolveApolloUrl } from "../lib/apollo_client.js";
5
5
  import { launchBrowser } from "../lib/browser_opener.js";
6
6
  // Resolve the URL to open (chosen endpoint > production > any > repository) and launch it.
7
- export async function runOpen(serviceName, options) {
8
- const catalogue = await fetchCatalogue(APOLLO_URL);
7
+ export async function runOpen(serviceName, options, globals) {
8
+ const baseUrl = resolveApolloUrl(globals);
9
+ const catalogue = await fetchCatalogue(baseUrl);
9
10
  const entry = (catalogue.services ?? []).find((service) => service.name === serviceName);
10
11
  if (!entry) {
11
- p.log.error(`'${serviceName}' is not in the catalogue at ${APOLLO_URL}.`);
12
+ p.log.error(`'${serviceName}' is not in the catalogue at ${baseUrl}.`);
12
13
  process.exitCode = 1;
13
14
  return;
14
15
  }
@@ -4,11 +4,12 @@
4
4
  // credentials disappear).
5
5
  import path from "node:path";
6
6
  import * as p from "@clack/prompts";
7
- import { APOLLO_URL, fetchAppCredentials } from "../lib/apollo_client.js";
7
+ import { fetchAppCredentials, resolveApolloUrl } from "../lib/apollo_client.js";
8
8
  import { readManagedValues, upsertManagedValues, writeManagedValues } from "../lib/env_file.js";
9
9
  // Run the sync flow: fetch the projection, rewrite the managed block wholly.
10
- export async function runSync() {
10
+ export async function runSync(options) {
11
11
  const cwd = process.cwd();
12
+ const baseUrl = resolveApolloUrl(options);
12
13
  p.intro("apollo sync");
13
14
  const managedValues = await readManagedValues(path.join(cwd, ".env"));
14
15
  const appKey = managedValues.APOLLO_APP_KEY ?? process.env.APOLLO_APP_KEY;
@@ -22,7 +23,7 @@ export async function runSync() {
22
23
  spinner.start("Fetching the application's credential projection");
23
24
  let projection;
24
25
  try {
25
- projection = await fetchAppCredentials(APOLLO_URL, appKey);
26
+ projection = await fetchAppCredentials(baseUrl, appKey);
26
27
  }
27
28
  catch (error) {
28
29
  spinner.stop("Projection failed", 1);
package/dist/index.js CHANGED
@@ -21,14 +21,15 @@ program
21
21
  .name("apollo")
22
22
  .description("Bootstrap NuStack services into your repo via the Apollo discovery plane.")
23
23
  .version(version)
24
+ .option("--apollo-url <url>", "Apollo base URL (defaults to $APOLLO_URL, then the hosted instance)")
24
25
  // Bare `apollo`: bootstrap — check sign-in, login if needed, then init.
25
- .action(async () => runBootstrap());
26
+ .action(async () => runBootstrap(program.opts()));
26
27
  program.addHelpText("after", "\nRun `apollo` with no command to bootstrap: it checks your sign-in, runs login if needed, then init.");
27
28
  program
28
29
  .command("login")
29
30
  .description("Sign in via the browser (24h device trust) and issue your APOLLO_APP_KEY")
30
31
  .option("--no-open", "don't auto-open the browser; just print the approval URL")
31
- .action(async (options) => runLogin(options));
32
+ .action(async (options) => runLogin(program.opts(), options));
32
33
  program
33
34
  .command("logout")
34
35
  .description("Sign out: remove APOLLO_APP_KEY from the managed .env block and sever device trust")
@@ -36,24 +37,24 @@ program
36
37
  program
37
38
  .command("init")
38
39
  .description("Store your APOLLO_APP_KEY in the managed .env block and scaffold the manifest")
39
- .action(async () => runInit());
40
+ .action(async () => runInit(program.opts()));
40
41
  program
41
42
  .command("add [service]")
42
43
  .description("Install a stack service: exchange credentials, write .env, install the package")
43
- .action(async (service) => runAdd(service));
44
+ .action(async (service) => runAdd(service, program.opts()));
44
45
  program
45
46
  .command("list")
46
47
  .description("List the services in the Apollo catalogue")
47
- .action(async () => runList());
48
+ .action(async () => runList(program.opts()));
48
49
  program
49
50
  .command("sync")
50
51
  .description("Re-project all credentials issued to this application into the managed .env block")
51
- .action(async () => runSync());
52
+ .action(async () => runSync(program.opts()));
52
53
  program
53
54
  .command("describe <service>")
54
55
  .description("Show the full catalogue entry for a service")
55
56
  .option("--json", "emit the raw catalogue entry as JSON")
56
- .action(async (service, options) => runDescribe(service, options));
57
+ .action(async (service, options) => runDescribe(service, options, program.opts()));
57
58
  program
58
59
  .command("validate [manifest]")
59
60
  .description("Validate an apollo.service.json (path or URL) against the apollo.dev/v1 schema")
@@ -63,14 +64,14 @@ program
63
64
  .command("graph")
64
65
  .description("Render the catalogue's consumes-graph")
65
66
  .option("--mermaid", "emit a mermaid `graph LR` block instead of ascii")
66
- .action(async (options) => runGraph(options));
67
+ .action(async (options) => runGraph(options, program.opts()));
67
68
  program
68
69
  .command("doctor")
69
70
  .description("Check key validity, credential health, and manifest conformance")
70
- .action(async () => runDoctor());
71
+ .action(async () => runDoctor(program.opts()));
71
72
  program
72
73
  .command("open <service>")
73
74
  .description("Open the service's endpoint (or repository) in the browser")
74
75
  .option("--env <name>", "pick a specific endpoint environment (default: production)")
75
- .action(async (service, options) => runOpen(service, options));
76
+ .action(async (service, options) => runOpen(service, options, program.opts()));
76
77
  await program.parseAsync(process.argv);
@@ -1,9 +1,11 @@
1
1
  // packages/cli/src/lib/apollo_client.ts - HTTP client for the Apollo discovery
2
2
  // plane: catalogue fetch + credential exchange + key validation.
3
- // The discovery plane lives at one hosted location (consumer-journey §9.1 —
4
- // location baked in). Hardcoded; no flag or env override.
5
- export const APOLLO_URL = "https://apollo.nurix-ai.co";
3
+ const DEFAULT_APOLLO_URL = "https://apollo.nurix-ai.co";
6
4
  const REQUEST_TIMEOUT_MS = 10_000;
5
+ // Resolve the Apollo base URL: --apollo-url flag > APOLLO_URL env > hosted default.
6
+ export function resolveApolloUrl(options) {
7
+ return (options.apolloUrl ?? process.env.APOLLO_URL ?? DEFAULT_APOLLO_URL).replace(/\/+$/, "");
8
+ }
7
9
  // Fetch the public discovery catalogue.
8
10
  export async function fetchCatalogue(baseUrl) {
9
11
  const response = await fetch(`${baseUrl}/api/catalog.json`, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/apollo",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "The apollo CLI — bootstrap NuStack services into a repo via the Apollo discovery plane.",
5
5
  "type": "module",
6
6
  "private": false,