@nurix/apollo 0.3.4 → 0.3.5

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
@@ -23,10 +23,17 @@ npm install -g @nurix/apollo # or install the `apollo` binary globally
23
23
  ## Commands (implemented)
24
24
 
25
25
  ```
26
+ apollo # bootstrap: validate the stored APOLLO_APP_KEY,
27
+ # run the login flow if it's missing/rejected,
28
+ # then finish with init's manifest scaffold
26
29
  apollo login [--no-open] # browser SSO via 24h device trust: approve once at
27
30
  # /device (Google SSO / OTP); re-logins within the
28
31
  # window need no browser. Issues APOLLO_APP_KEY +
29
32
  # APOLLO_APP_ID into the managed block.
33
+ apollo logout # local sign-out: drop APOLLO_APP_KEY from the
34
+ # managed block + sever device trust (next login
35
+ # needs browser approval); the key stays valid
36
+ # server-side until revoked in the console
30
37
  apollo init # login (or paste a key), gitignore .env,
31
38
  # scaffold apollo.service.json
32
39
  apollo add <service> # the installer (see flow below)
@@ -46,8 +53,8 @@ apollo doctor # key validity + per-credential health probes +
46
53
  apollo open <svc> [--env <name>] # open endpoint (default production) or repo URL
47
54
  ```
48
55
 
49
- Global options: `--apollo-url <url>` (defaults to `$APOLLO_URL`, then the
50
- hosted instance `https://apollo.nurix-ai.co`).
56
+ The Apollo base URL is hardcoded to the hosted instance
57
+ (`https://apollo.nurix-ai.co`) — there is no flag or env override.
51
58
 
52
59
  ### `apollo init`
53
60
 
@@ -4,15 +4,14 @@
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 { exchangeServiceCredential, fetchCatalogue, resolveApolloUrl, } from "../lib/apollo_client.js";
7
+ import { APOLLO_URL, exchangeServiceCredential, fetchCatalogue, } 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, options) {
13
+ export async function runAdd(serviceArg) {
14
14
  const cwd = process.cwd();
15
- const baseUrl = resolveApolloUrl(options);
16
15
  p.intro(`apollo add${serviceArg ? ` ${serviceArg}` : ""}`);
17
16
  const managedValues = await readManagedValues(path.join(cwd, ".env"));
18
17
  const appKey = managedValues.APOLLO_APP_KEY ?? process.env.APOLLO_APP_KEY;
@@ -24,7 +23,7 @@ export async function runAdd(serviceArg, options) {
24
23
  }
25
24
  const spinner = p.spinner();
26
25
  spinner.start("Fetching the catalogue");
27
- const catalogue = await fetchCatalogue(baseUrl).catch(() => null);
26
+ const catalogue = await fetchCatalogue(APOLLO_URL).catch(() => null);
28
27
  const services = catalogue?.services ?? [];
29
28
  spinner.stop(`Catalogue loaded: ${services.length} service(s)`);
30
29
  const serviceName = serviceArg ?? (await pickService(services));
@@ -40,7 +39,7 @@ export async function runAdd(serviceArg, options) {
40
39
  spinner.start(`Exchanging credentials for '${serviceName}'`);
41
40
  let credential;
42
41
  try {
43
- credential = await exchangeServiceCredential(baseUrl, appKey, serviceName);
42
+ credential = await exchangeServiceCredential(APOLLO_URL, appKey, serviceName);
44
43
  }
45
44
  catch (error) {
46
45
  spinner.stop("Exchange failed", 1);
@@ -0,0 +1,40 @@
1
+ // packages/cli/src/commands/bootstrap.ts - Bare `apollo` (no subcommand): the
2
+ // zero-to-ready flow. Probes the stored APOLLO_APP_KEY; when it's missing or
3
+ // rejected, runs the login flow, then finishes with init's manifest scaffold.
4
+ import path from "node:path";
5
+ import * as p from "@clack/prompts";
6
+ import { APOLLO_URL, validateAppKey } from "../lib/apollo_client.js";
7
+ import { readManagedValues } from "../lib/env_file.js";
8
+ import { isInteractive } from "../lib/interactive.js";
9
+ import { performDeviceLogin } from "./login.js";
10
+ import { finishInit } from "./init.js";
11
+ // Run the bootstrap flow: ensure a valid app key (login when needed), then init.
12
+ export async function runBootstrap() {
13
+ const cwd = process.cwd();
14
+ p.intro("apollo");
15
+ if (!isInteractive()) {
16
+ p.log.error("Bare `apollo` needs an interactive terminal (browser sign-in + prompts).");
17
+ p.outro("In scripted contexts run `apollo login --no-open`, then `apollo init`.");
18
+ process.exitCode = 1;
19
+ return;
20
+ }
21
+ const managedValues = await readManagedValues(path.join(cwd, ".env"));
22
+ let isSignedIn = false;
23
+ if (managedValues.APOLLO_APP_KEY) {
24
+ const spinner = p.spinner();
25
+ spinner.start("Checking the stored APOLLO_APP_KEY against Apollo");
26
+ isSignedIn = await validateAppKey(APOLLO_URL, managedValues.APOLLO_APP_KEY).catch(() => false);
27
+ spinner.stop(isSignedIn
28
+ ? "Signed in — the stored key is valid"
29
+ : "Stored key rejected — signing in again", isSignedIn ? 0 : 1);
30
+ }
31
+ else {
32
+ p.log.info("No APOLLO_APP_KEY in the managed .env block — signing in");
33
+ }
34
+ if (!isSignedIn && !(await performDeviceLogin(cwd))) {
35
+ p.outro("Sign-in failed — nothing was written. Run `apollo` again, or `apollo login` to retry.");
36
+ process.exitCode = 1;
37
+ return;
38
+ }
39
+ await finishInit(cwd);
40
+ }
@@ -1,14 +1,13 @@
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 { fetchCatalogue, resolveApolloUrl } from "../lib/apollo_client.js";
4
+ import { APOLLO_URL, fetchCatalogue } 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, globals) {
7
- const baseUrl = resolveApolloUrl(globals);
8
- const catalogue = await fetchCatalogue(baseUrl);
6
+ export async function runDescribe(serviceName, options) {
7
+ const catalogue = await fetchCatalogue(APOLLO_URL);
9
8
  const entry = (catalogue.services ?? []).find((service) => service.name === serviceName);
10
9
  if (!entry) {
11
- p.log.error(`'${serviceName}' is not in the catalogue at ${baseUrl} (${catalogue.services?.length ?? 0} services listed).`);
10
+ p.log.error(`'${serviceName}' is not in the catalogue at ${APOLLO_URL} (${catalogue.services?.length ?? 0} services listed).`);
12
11
  process.exitCode = 1;
13
12
  return;
14
13
  }
@@ -5,20 +5,19 @@
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 { fetchCatalogue, resolveApolloUrl, validateAppKey, } from "../lib/apollo_client.js";
8
+ import { APOLLO_URL, fetchCatalogue, 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(globals) {
13
+ export async function runDoctor() {
14
14
  const cwd = process.cwd();
15
- const baseUrl = resolveApolloUrl(globals);
16
15
  p.intro("apollo doctor");
17
16
  const counters = { errors: 0, warnings: 0 };
18
17
  const managedValues = await readManagedValues(path.join(cwd, ".env"));
19
- await checkAppKey(managedValues, baseUrl, counters);
18
+ await checkAppKey(managedValues, counters);
20
19
  await checkServiceCredentials(managedValues, counters);
21
- await checkManifest(cwd, baseUrl, counters);
20
+ await checkManifest(cwd, counters);
22
21
  const summary = `${counters.errors} error(s), ${counters.warnings} warning(s)`;
23
22
  if (counters.errors > 0) {
24
23
  p.outro(`Unhealthy — ${summary}.`);
@@ -29,14 +28,14 @@ export async function runDoctor(globals) {
29
28
  }
30
29
  }
31
30
  // Check the APOLLO_APP_KEY exists and is still accepted by Apollo.
32
- async function checkAppKey(managedValues, baseUrl, counters) {
31
+ async function checkAppKey(managedValues, counters) {
33
32
  const appKey = managedValues.APOLLO_APP_KEY ?? process.env.APOLLO_APP_KEY;
34
33
  if (!appKey) {
35
34
  p.log.error("APOLLO_APP_KEY missing — run `apollo init`.");
36
35
  counters.errors += 1;
37
36
  return;
38
37
  }
39
- const isValid = await validateAppKey(baseUrl, appKey).catch(() => false);
38
+ const isValid = await validateAppKey(APOLLO_URL, appKey).catch(() => false);
40
39
  if (isValid) {
41
40
  p.log.success("APOLLO_APP_KEY accepted by Apollo");
42
41
  }
@@ -81,7 +80,7 @@ async function checkServiceCredentials(managedValues, counters) {
81
80
  }
82
81
  }
83
82
  // Validate the manifest and cross-check its consumes edges against the catalogue.
84
- async function checkManifest(cwd, baseUrl, counters) {
83
+ async function checkManifest(cwd, counters) {
85
84
  if (!hasManifest(cwd)) {
86
85
  p.log.warn("No apollo.service.json — the app has no bill of materials. `apollo init` scaffolds one.");
87
86
  counters.warnings += 1;
@@ -110,7 +109,7 @@ async function checkManifest(cwd, baseUrl, counters) {
110
109
  const edges = (manifest.services ?? []).flatMap((service) => service.consumes ?? []);
111
110
  if (edges.length === 0)
112
111
  return;
113
- const catalogue = await fetchCatalogue(baseUrl).catch(() => null);
112
+ const catalogue = await fetchCatalogue(APOLLO_URL).catch(() => null);
114
113
  const services = catalogue?.services ?? [];
115
114
  if (services.length === 0) {
116
115
  p.log.info("Catalogue empty — consumes edges not cross-checked.");
@@ -1,10 +1,9 @@
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 { fetchCatalogue, resolveApolloUrl } from "../lib/apollo_client.js";
3
+ import { APOLLO_URL, fetchCatalogue } from "../lib/apollo_client.js";
4
4
  // Print every consumes edge in the catalogue; --mermaid emits `graph LR`.
5
- export async function runGraph(options, globals) {
6
- const baseUrl = resolveApolloUrl(globals);
7
- const catalogue = await fetchCatalogue(baseUrl);
5
+ export async function runGraph(options) {
6
+ const catalogue = await fetchCatalogue(APOLLO_URL);
8
7
  const services = catalogue.services ?? [];
9
8
  const edges = services.flatMap((service) => (service.consumes ?? []).map((edge) => ({
10
9
  from: service.name,
@@ -12,7 +11,7 @@ export async function runGraph(options, globals) {
12
11
  via: edge.via ?? "http",
13
12
  })));
14
13
  if (services.length === 0) {
15
- console.log(`Catalogue at ${baseUrl} is empty — nothing to graph.`);
14
+ console.log(`Catalogue at ${APOLLO_URL} is empty — nothing to graph.`);
16
15
  return;
17
16
  }
18
17
  if (edges.length === 0) {
@@ -3,15 +3,14 @@
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 { resolveApolloUrl, validateAppKey } from "../lib/apollo_client.js";
6
+ import { APOLLO_URL, 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(options) {
12
+ export async function runInit() {
13
13
  const cwd = process.cwd();
14
- const baseUrl = resolveApolloUrl(options);
15
14
  p.intro("apollo init");
16
15
  if (!isInteractive()) {
17
16
  p.log.error("apollo init needs an interactive terminal (it prompts for the key).");
@@ -33,7 +32,7 @@ export async function runInit(options) {
33
32
  message: "Sign in via the browser to create your application & key? (No = paste a key)",
34
33
  });
35
34
  if (!p.isCancel(viaBrowser) && viaBrowser) {
36
- if (!(await performDeviceLogin(baseUrl, cwd))) {
35
+ if (!(await performDeviceLogin(cwd))) {
37
36
  p.outro("Sign-in failed — re-run `apollo init`, or pick the paste option.");
38
37
  process.exitCode = 1;
39
38
  return;
@@ -42,7 +41,7 @@ export async function runInit(options) {
42
41
  return;
43
42
  }
44
43
  const appKey = await p.password({
45
- message: `Paste your APOLLO_APP_KEY (create an application in the Apollo console: ${baseUrl})`,
44
+ message: `Paste your APOLLO_APP_KEY (create an application in the Apollo console: ${APOLLO_URL})`,
46
45
  validate: (value) => value?.startsWith("apollo_app_") ? undefined : "Expected a key starting with apollo_app_",
47
46
  });
48
47
  if (p.isCancel(appKey)) {
@@ -51,7 +50,7 @@ export async function runInit(options) {
51
50
  }
52
51
  const spinner = p.spinner();
53
52
  spinner.start("Validating the key against Apollo");
54
- const isValid = await validateAppKey(baseUrl, appKey).catch(() => false);
53
+ const isValid = await validateAppKey(APOLLO_URL, appKey).catch(() => false);
55
54
  if (!isValid) {
56
55
  spinner.stop("Key rejected by Apollo", 1);
57
56
  p.outro("No key, no install — fix the key and re-run `apollo init`.");
@@ -66,8 +65,9 @@ export async function runInit(options) {
66
65
  }
67
66
  await finishInit(cwd);
68
67
  }
69
- // Shared init tail: offer the manifest scaffold, then close out.
70
- async function finishInit(cwd) {
68
+ // Shared init tail (also the bootstrap command's closing step): offer the
69
+ // manifest scaffold, then close out.
70
+ export async function finishInit(cwd) {
71
71
  if (!hasManifest(cwd)) {
72
72
  const repoName = deriveRepoName(cwd);
73
73
  const shouldScaffold = await p.confirm({
@@ -1,13 +1,12 @@
1
1
  // packages/cli/src/commands/list.ts - `apollo list`: print the discovery
2
2
  // catalogue, one line per service.
3
- import { fetchCatalogue, resolveApolloUrl } from "../lib/apollo_client.js";
3
+ import { APOLLO_URL, fetchCatalogue } from "../lib/apollo_client.js";
4
4
  // Print the catalogue as aligned columns: name, classifier, status, description.
5
- export async function runList(options) {
6
- const baseUrl = resolveApolloUrl(options);
7
- const catalogue = await fetchCatalogue(baseUrl);
5
+ export async function runList() {
6
+ const catalogue = await fetchCatalogue(APOLLO_URL);
8
7
  const services = catalogue.services ?? [];
9
8
  if (services.length === 0) {
10
- console.log(`Catalogue at ${baseUrl} is empty — the registry read path / refresh isn't wired yet.`);
9
+ console.log(`Catalogue at ${APOLLO_URL} is empty — the registry read path / refresh isn't wired yet.`);
11
10
  return;
12
11
  }
13
12
  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 { resolveApolloUrl } from "../lib/apollo_client.js";
8
+ import { APOLLO_URL } 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(baseUrl, cwd, flags = {}) {
16
+ export async function performDeviceLogin(cwd, flags = {}) {
17
17
  const deviceId = await getOrCreateDeviceId();
18
- const start = await startDeviceTrust(baseUrl, deviceId);
18
+ const start = await startDeviceTrust(APOLLO_URL, 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(baseUrl, 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(baseUrl, pollParams);
29
+ const firstAttempt = await pollDeviceOnce(APOLLO_URL, 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(baseUrl, 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(baseUrl, pollParams, start.interval, Math.min(start.expires_in, APPROVAL_TIMEOUT_SECONDS));
46
+ issued = await pollUntilIssued(APOLLO_URL, 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,11 +68,10 @@ export async function performDeviceLogin(baseUrl, cwd, flags = {}) {
68
68
  return true;
69
69
  }
70
70
  // `apollo login` — the standalone command wrapper.
71
- export async function runLogin(options, flags = {}) {
71
+ export async function runLogin(flags = {}) {
72
72
  const cwd = process.cwd();
73
- const baseUrl = resolveApolloUrl(options);
74
73
  p.intro("apollo login");
75
- const didLogin = await performDeviceLogin(baseUrl, cwd, flags);
74
+ const didLogin = await performDeviceLogin(cwd, flags);
76
75
  if (!didLogin) {
77
76
  p.outro("Nothing was written.");
78
77
  process.exitCode = 1;
@@ -0,0 +1,35 @@
1
+ // packages/cli/src/commands/logout.ts - `apollo logout`: the inverse of login.
2
+ // Removes APOLLO_APP_KEY from the managed .env block and clears the local
3
+ // device identity so the next login needs a fresh browser approval. Local
4
+ // only — there is no self-revoke endpoint yet, so the issued key stays valid
5
+ // server-side until revoked in the Apollo console.
6
+ import path from "node:path";
7
+ import * as p from "@clack/prompts";
8
+ import { readManagedValues, removeManagedValues } from "../lib/env_file.js";
9
+ import { clearDeviceId } from "../lib/device_identity.js";
10
+ // Run the logout flow: drop the personal key locally and sever device trust.
11
+ export async function runLogout() {
12
+ const cwd = process.cwd();
13
+ p.intro("apollo logout");
14
+ const removedKeys = await removeManagedValues(path.join(cwd, ".env"), ["APOLLO_APP_KEY"]);
15
+ const didClearDevice = await clearDeviceId();
16
+ if (removedKeys.length === 0 && !didClearDevice) {
17
+ p.outro("Nothing to do — no APOLLO_APP_KEY here and no device identity stored.");
18
+ return;
19
+ }
20
+ if (removedKeys.length > 0) {
21
+ p.log.success("Removed APOLLO_APP_KEY from the managed .env block");
22
+ p.log.warn("The key itself stays valid server-side — revoke it in the Apollo console if it shouldn't.");
23
+ }
24
+ else {
25
+ p.log.info("No APOLLO_APP_KEY in the managed .env block.");
26
+ }
27
+ if (didClearDevice) {
28
+ p.log.success("Cleared the device identity — the next login needs browser approval");
29
+ }
30
+ const remainingValues = await readManagedValues(path.join(cwd, ".env"));
31
+ if (remainingValues.APOLLO_APP_ID) {
32
+ p.log.info("Kept APOLLO_APP_ID — the next login re-binds to the same application.");
33
+ }
34
+ p.outro("Signed out. Run `apollo login` to sign back in.");
35
+ }
@@ -1,15 +1,14 @@
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 { fetchCatalogue, resolveApolloUrl } from "../lib/apollo_client.js";
4
+ import { APOLLO_URL, fetchCatalogue } 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, globals) {
8
- const baseUrl = resolveApolloUrl(globals);
9
- const catalogue = await fetchCatalogue(baseUrl);
7
+ export async function runOpen(serviceName, options) {
8
+ const catalogue = await fetchCatalogue(APOLLO_URL);
10
9
  const entry = (catalogue.services ?? []).find((service) => service.name === serviceName);
11
10
  if (!entry) {
12
- p.log.error(`'${serviceName}' is not in the catalogue at ${baseUrl}.`);
11
+ p.log.error(`'${serviceName}' is not in the catalogue at ${APOLLO_URL}.`);
13
12
  process.exitCode = 1;
14
13
  return;
15
14
  }
@@ -4,12 +4,11 @@
4
4
  // credentials disappear).
5
5
  import path from "node:path";
6
6
  import * as p from "@clack/prompts";
7
- import { fetchAppCredentials, resolveApolloUrl } from "../lib/apollo_client.js";
7
+ import { APOLLO_URL, fetchAppCredentials } 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(options) {
10
+ export async function runSync() {
11
11
  const cwd = process.cwd();
12
- const baseUrl = resolveApolloUrl(options);
13
12
  p.intro("apollo sync");
14
13
  const managedValues = await readManagedValues(path.join(cwd, ".env"));
15
14
  const appKey = managedValues.APOLLO_APP_KEY ?? process.env.APOLLO_APP_KEY;
@@ -23,7 +22,7 @@ export async function runSync(options) {
23
22
  spinner.start("Fetching the application's credential projection");
24
23
  let projection;
25
24
  try {
26
- projection = await fetchAppCredentials(baseUrl, appKey);
25
+ projection = await fetchAppCredentials(APOLLO_URL, appKey);
27
26
  }
28
27
  catch (error) {
29
28
  spinner.stop("Projection failed", 1);
package/dist/index.js CHANGED
@@ -12,6 +12,8 @@ import { runValidate } from "./commands/validate.js";
12
12
  import { runGraph } from "./commands/graph.js";
13
13
  import { runDoctor } from "./commands/doctor.js";
14
14
  import { runOpen } from "./commands/open.js";
15
+ import { runBootstrap } from "./commands/bootstrap.js";
16
+ import { runLogout } from "./commands/logout.js";
15
17
  // Single version source: package.json (npm ships it alongside dist/).
16
18
  const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
17
19
  const program = new Command();
@@ -19,33 +21,39 @@ program
19
21
  .name("apollo")
20
22
  .description("Bootstrap NuStack services into your repo via the Apollo discovery plane.")
21
23
  .version(version)
22
- .option("--apollo-url <url>", "Apollo base URL (defaults to $APOLLO_URL, then the hosted instance)");
24
+ // Bare `apollo`: bootstrap check sign-in, login if needed, then init.
25
+ .action(async () => runBootstrap());
26
+ program.addHelpText("after", "\nRun `apollo` with no command to bootstrap: it checks your sign-in, runs login if needed, then init.");
23
27
  program
24
28
  .command("login")
25
29
  .description("Sign in via the browser (24h device trust) and issue your APOLLO_APP_KEY")
26
30
  .option("--no-open", "don't auto-open the browser; just print the approval URL")
27
- .action(async (options) => runLogin(program.opts(), options));
31
+ .action(async (options) => runLogin(options));
32
+ program
33
+ .command("logout")
34
+ .description("Sign out: remove APOLLO_APP_KEY from the managed .env block and sever device trust")
35
+ .action(async () => runLogout());
28
36
  program
29
37
  .command("init")
30
38
  .description("Store your APOLLO_APP_KEY in the managed .env block and scaffold the manifest")
31
- .action(async () => runInit(program.opts()));
39
+ .action(async () => runInit());
32
40
  program
33
41
  .command("add [service]")
34
42
  .description("Install a stack service: exchange credentials, write .env, install the package")
35
- .action(async (service) => runAdd(service, program.opts()));
43
+ .action(async (service) => runAdd(service));
36
44
  program
37
45
  .command("list")
38
46
  .description("List the services in the Apollo catalogue")
39
- .action(async () => runList(program.opts()));
47
+ .action(async () => runList());
40
48
  program
41
49
  .command("sync")
42
50
  .description("Re-project all credentials issued to this application into the managed .env block")
43
- .action(async () => runSync(program.opts()));
51
+ .action(async () => runSync());
44
52
  program
45
53
  .command("describe <service>")
46
54
  .description("Show the full catalogue entry for a service")
47
55
  .option("--json", "emit the raw catalogue entry as JSON")
48
- .action(async (service, options) => runDescribe(service, options, program.opts()));
56
+ .action(async (service, options) => runDescribe(service, options));
49
57
  program
50
58
  .command("validate [manifest]")
51
59
  .description("Validate an apollo.service.json (path or URL) against the apollo.dev/v1 schema")
@@ -55,14 +63,14 @@ program
55
63
  .command("graph")
56
64
  .description("Render the catalogue's consumes-graph")
57
65
  .option("--mermaid", "emit a mermaid `graph LR` block instead of ascii")
58
- .action(async (options) => runGraph(options, program.opts()));
66
+ .action(async (options) => runGraph(options));
59
67
  program
60
68
  .command("doctor")
61
69
  .description("Check key validity, credential health, and manifest conformance")
62
- .action(async () => runDoctor(program.opts()));
70
+ .action(async () => runDoctor());
63
71
  program
64
72
  .command("open <service>")
65
73
  .description("Open the service's endpoint (or repository) in the browser")
66
74
  .option("--env <name>", "pick a specific endpoint environment (default: production)")
67
- .action(async (service, options) => runOpen(service, options, program.opts()));
75
+ .action(async (service, options) => runOpen(service, options));
68
76
  await program.parseAsync(process.argv);
@@ -1,11 +1,9 @@
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
- const DEFAULT_APOLLO_URL = "https://apollo.nurix-ai.co";
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";
4
6
  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
- }
9
7
  // Fetch the public discovery catalogue.
10
8
  export async function fetchCatalogue(baseUrl) {
11
9
  const response = await fetch(`${baseUrl}/api/catalog.json`, {
@@ -1,15 +1,19 @@
1
1
  // packages/cli/src/lib/device_identity.ts - Stable per-machine device identity
2
2
  // for the 24h device-trust window. Generated once, persisted at
3
3
  // ~/.apollo/device_id, and sent with every device/start + device/poll call.
4
- import { mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
5
5
  import { existsSync } from "node:fs";
6
6
  import { randomBytes } from "node:crypto";
7
7
  import os from "node:os";
8
8
  import path from "node:path";
9
+ // Absolute path of the persisted device id file.
10
+ function deviceIdFilePath() {
11
+ return path.join(os.homedir(), ".apollo", "device_id");
12
+ }
9
13
  // Read the persisted device id, creating one on first use.
10
14
  export async function getOrCreateDeviceId() {
11
- const dir = path.join(os.homedir(), ".apollo");
12
- const file = path.join(dir, "device_id");
15
+ const file = deviceIdFilePath();
16
+ const dir = path.dirname(file);
13
17
  if (existsSync(file)) {
14
18
  const existing = (await readFile(file, "utf8")).trim();
15
19
  if (existing)
@@ -20,3 +24,11 @@ export async function getOrCreateDeviceId() {
20
24
  await writeFile(file, `${deviceId}\n`, "utf8");
21
25
  return deviceId;
22
26
  }
27
+ // Delete the persisted device id (severs the 24h trust locally); true when one existed.
28
+ export async function clearDeviceId() {
29
+ const file = deviceIdFilePath();
30
+ if (!existsSync(file))
31
+ return false;
32
+ await rm(file, { force: true });
33
+ return true;
34
+ }
@@ -39,6 +39,17 @@ export async function writeManagedValues(filePath, values) {
39
39
  const existingContent = existsSync(filePath) ? await readFile(filePath, "utf8") : "";
40
40
  await writeFile(filePath, replaceBlock(existingContent, block), "utf8");
41
41
  }
42
+ // Remove keys from the managed block; returns the keys actually removed.
43
+ export async function removeManagedValues(filePath, keys) {
44
+ const currentValues = await readManagedValues(filePath);
45
+ const removedKeys = keys.filter((key) => key in currentValues);
46
+ if (removedKeys.length === 0)
47
+ return [];
48
+ for (const key of removedKeys)
49
+ delete currentValues[key];
50
+ await writeManagedValues(filePath, currentValues);
51
+ return removedKeys;
52
+ }
42
53
  // Ensure .gitignore covers .env so managed secrets never get committed.
43
54
  export async function ensureEnvGitignored(cwd) {
44
55
  const gitignorePath = path.join(cwd, ".gitignore");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nurix/apollo",
3
- "version": "0.3.4",
3
+ "version": "0.3.5",
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,