@nessielabs/daemon 0.4.0 → 0.5.26
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 +8 -17
- package/dist/cli.js +42 -21
- package/dist/commands/auth.js +116 -0
- package/dist/commands/integrations.js +84 -0
- package/dist/commands/login.js +145 -0
- package/dist/commands/run.js +21 -6
- package/dist/commands/service.js +18 -36
- package/dist/commands/sharing.js +399 -0
- package/dist/commands/status.js +67 -0
- package/dist/commands/teams.js +64 -0
- package/dist/nera/client.js +156 -0
- package/dist/nera/process.js +78 -0
- package/dist/nera/runtimeFile.js +25 -0
- package/dist/platform/binary.js +21 -0
- package/dist/platform/openBrowser.js +14 -0
- package/dist/platform/paths.js +13 -0
- package/dist/platform/table.js +85 -0
- package/package.json +9 -5
- package/vendor/nera/darwin-arm64/nera +0 -0
- package/vendor/nera/darwin-x64/nera +0 -0
- package/vendor/nera/linux-arm64/nera +0 -0
- package/vendor/nera/linux-x64/nera +0 -0
- package/vendor/nera/win32-arm64/nera.exe +0 -0
- package/vendor/nera/win32-x64/nera.exe +0 -0
- package/vendor/proto/nessie.edge.v1.proto +475 -0
- package/dist/api/http.js +0 -44
- package/dist/auth/deviceFlow.js +0 -36
- package/dist/auth/tokens.js +0 -30
- package/dist/commands/setup.js +0 -75
- package/dist/config/endpoints.js +0 -2
- package/dist/config/paths.js +0 -19
- package/dist/config/store.js +0 -27
- package/dist/git/repoResolver.js +0 -215
- package/dist/localStore/daemonGraphStore.js +0 -494
- package/dist/localStore/messageComparison.js +0 -37
- package/dist/localStore/pushMapping.js +0 -69
- package/dist/localStore/rowTypes.js +0 -1
- package/dist/localStore/schema.js +0 -70
- package/dist/localStore/uuid.js +0 -12
- package/dist/parser/claudeCode.js +0 -202
- package/dist/parser/codex.js +0 -91
- package/dist/parser/jsonl.js +0 -24
- package/dist/parser/types.js +0 -15
- package/dist/redact/secrets.js +0 -60
- package/dist/service/process.js +0 -134
- package/dist/service/systemd.js +0 -38
- package/dist/sources/detect.js +0 -43
- package/dist/sync/batch.js +0 -50
- package/dist/sync/client.js +0 -90
- package/dist/sync/exclusions.js +0 -10
- package/dist/sync/hash.js +0 -4
- package/dist/sync/loop.js +0 -106
- package/dist/sync/scan.js +0 -245
- package/dist/sync/slices.js +0 -63
- package/dist/sync/types.js +0 -1
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# Nessie Daemon
|
|
2
2
|
|
|
3
|
-
Headless
|
|
3
|
+
Headless Nessie command line interface.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install -g @nessielabs/daemon
|
|
9
|
-
nessie-daemon
|
|
9
|
+
nessie-daemon login
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
`
|
|
12
|
+
`login` starts Nessie, signs in through the browser, detects local agent history directories, lets you configure data integrations, and starts ingestion plus cloud sync.
|
|
13
13
|
|
|
14
14
|
For non-interactive headless setup, pass a Nessie API key:
|
|
15
15
|
|
|
@@ -21,6 +21,7 @@ nessie-daemon start
|
|
|
21
21
|
## Commands
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
+
nessie-daemon login
|
|
24
25
|
nessie-daemon setup
|
|
25
26
|
nessie-daemon run --once
|
|
26
27
|
nessie-daemon start
|
|
@@ -28,22 +29,12 @@ nessie-daemon stop
|
|
|
28
29
|
nessie-daemon status
|
|
29
30
|
```
|
|
30
31
|
|
|
31
|
-
The
|
|
32
|
-
|
|
33
|
-
Config is stored at `~/.config/nessie-daemon/config.json`; the local sync graph is stored at `~/.local/state/nessie-daemon/nessie.sqlite`.
|
|
32
|
+
The package installs only the `nessie-daemon` binary to avoid colliding with the Nessie Mac CLI.
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
The npm daemon package embeds the Nessie runtime for the current platform. Node is a one-shot CLI wrapper: commands start or configure Nessie and then exit, while the background service remains running.
|
|
36
35
|
|
|
37
36
|
## systemd
|
|
38
37
|
|
|
39
|
-
`
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
~/.config/systemd/user/nessie-daemon.service
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
On long-running headless VMs, enable user services to survive logout:
|
|
38
|
+
`nessie-daemon start` starts Nessie, enables ingestion and cloud sync timers, and exits. `nessie-daemon run` does the same for backwards compatibility. Logs are written under the user-local daemon state directory.
|
|
46
39
|
|
|
47
|
-
|
|
48
|
-
sudo loginctl enable-linger "$USER"
|
|
49
|
-
```
|
|
40
|
+
Existing user systemd units that invoke `nessie-daemon run` will only start or probe Nessie and then exit. They are no longer required and should be disabled or removed. This package no longer writes new systemd unit files itself.
|
package/dist/cli.js
CHANGED
|
@@ -1,40 +1,61 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { Command } from "commander";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
import { registerAuthCommands } from "./commands/auth.js";
|
|
5
|
+
import { registerIntegrationCommands } from "./commands/integrations.js";
|
|
6
|
+
import { login } from "./commands/login.js";
|
|
7
|
+
import { run } from "./commands/run.js";
|
|
8
|
+
import { service } from "./commands/service.js";
|
|
9
|
+
import { registerSharingCommands } from "./commands/sharing.js";
|
|
10
|
+
import { status } from "./commands/status.js";
|
|
11
|
+
import { registerTeamCommands } from "./commands/teams.js";
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
12
13
|
const { version } = require("../package.json");
|
|
13
14
|
const program = new Command();
|
|
14
15
|
program
|
|
15
16
|
.name("nessie-daemon")
|
|
16
|
-
.description("
|
|
17
|
+
.description("Nessie command line interface.")
|
|
17
18
|
.version(version);
|
|
18
19
|
program
|
|
19
|
-
.command("
|
|
20
|
-
.
|
|
20
|
+
.command("login")
|
|
21
|
+
.alias("setup")
|
|
22
|
+
.description("Sign in, configure data integrations, and start Nessie.")
|
|
21
23
|
.option("--api-key <key>", "Use a Nessie API key for non-interactive headless setup.")
|
|
22
|
-
.
|
|
24
|
+
.option("--no-open", "Print the browser sign-in URL instead of opening it.")
|
|
25
|
+
.option("--no-background", "Do not start the background service after setup.")
|
|
26
|
+
.action(login);
|
|
23
27
|
program
|
|
24
28
|
.command("run")
|
|
25
|
-
.description("
|
|
26
|
-
.option("--once", "
|
|
27
|
-
.option("--interval-seconds <seconds>", "
|
|
28
|
-
.action(
|
|
29
|
+
.description("Start Nessie and enable background processing.")
|
|
30
|
+
.option("--once", "Trigger one ingestion and cloud-sync cycle, then exit.")
|
|
31
|
+
.option("--interval-seconds <seconds>", "Ignored compatibility option from the old daemon.")
|
|
32
|
+
.action(run);
|
|
33
|
+
registerAuthCommands(program);
|
|
34
|
+
registerIntegrationCommands(program);
|
|
35
|
+
registerSharingCommands(program);
|
|
36
|
+
registerTeamCommands(program);
|
|
29
37
|
program
|
|
30
38
|
.command("start")
|
|
31
|
-
.description("Start the
|
|
32
|
-
.option("--background", "
|
|
33
|
-
.action((
|
|
39
|
+
.description("Start Nessie in the background.")
|
|
40
|
+
.option("--background", "Compatibility flag; background mode is the default.")
|
|
41
|
+
.action(async () => printIfPresent(await service("start")));
|
|
34
42
|
for (const commandName of ["stop", "status"]) {
|
|
35
43
|
program
|
|
36
44
|
.command(commandName)
|
|
37
|
-
.description(`${commandName[0]?.toUpperCase()}${commandName.slice(1)}
|
|
38
|
-
.action(() =>
|
|
45
|
+
.description(`${commandName[0]?.toUpperCase()}${commandName.slice(1)} Nessie.`)
|
|
46
|
+
.action(async () => commandName === "status" ? status() : printIfPresent(await service(commandName)));
|
|
39
47
|
}
|
|
40
|
-
|
|
48
|
+
function printIfPresent(message) {
|
|
49
|
+
if (message.length > 0)
|
|
50
|
+
console.log(message);
|
|
51
|
+
}
|
|
52
|
+
// Node 26 can warn about unsettled top-level await while command actions are
|
|
53
|
+
// waiting on the runtime; keep the process alive until commander settles.
|
|
54
|
+
const keepAlive = setTimeout(() => undefined, 2 ** 31 - 1);
|
|
55
|
+
program
|
|
56
|
+
.parseAsync()
|
|
57
|
+
.catch((error) => {
|
|
58
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
})
|
|
61
|
+
.finally(() => clearTimeout(keepAlive));
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import { AuthStatus, beginSignIn, getAuthState, setApiKey, signOut, startDevicePairing, switchAccount } from "../nera/client.js";
|
|
3
|
+
import { ensureRuntime } from "../nera/process.js";
|
|
4
|
+
import { openBrowser } from "../platform/openBrowser.js";
|
|
5
|
+
import { renderTabTable } from "../platform/table.js";
|
|
6
|
+
import { billingLines } from "./status.js";
|
|
7
|
+
export function registerAuthCommands(program) {
|
|
8
|
+
const auth = program.command("auth").description("Manage Nessie sign-in.");
|
|
9
|
+
auth.command("status").description("Show signed-in accounts.").action(authStatus);
|
|
10
|
+
auth
|
|
11
|
+
.command("login")
|
|
12
|
+
.description("Sign in to Nessie.")
|
|
13
|
+
.option("--api-key <key>", "Use a Nessie API key for non-interactive headless setup.")
|
|
14
|
+
.option("--pair", "Use device pairing instead of browser sign-in.")
|
|
15
|
+
.option("--no-open", "Print the browser sign-in URL instead of opening it.")
|
|
16
|
+
.action(authLogin);
|
|
17
|
+
auth.command("switch <user-id>").description("Switch active account.").action(authSwitch);
|
|
18
|
+
auth.command("logout [user-id]").description("Sign out the active account or a specific account.").action(authLogout);
|
|
19
|
+
}
|
|
20
|
+
async function authStatus() {
|
|
21
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
22
|
+
const auth = await getAuthState(runtime);
|
|
23
|
+
const accounts = auth.accounts ?? [];
|
|
24
|
+
if (accounts.length === 0) {
|
|
25
|
+
console.log("Not signed in.");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const rows = [
|
|
29
|
+
["ACTIVE", "EMAIL", "ID"],
|
|
30
|
+
...accounts.map((account) => [account.id === auth.user?.id ? "*" : "", account.email, account.id]),
|
|
31
|
+
];
|
|
32
|
+
const colors = [
|
|
33
|
+
["bold", "bold", "bold"],
|
|
34
|
+
...accounts.map((account) => [account.id === auth.user?.id ? "green" : "dim", undefined, "dim"]),
|
|
35
|
+
];
|
|
36
|
+
process.stdout.write(renderTabTable(rows, colors));
|
|
37
|
+
for (const line of billingLines(auth.billing))
|
|
38
|
+
console.log(line);
|
|
39
|
+
}
|
|
40
|
+
async function authLogin(options) {
|
|
41
|
+
if (options.apiKey !== undefined && options.pair === true)
|
|
42
|
+
throw new Error("--api-key and --pair cannot be used together.");
|
|
43
|
+
if (options.apiKey !== undefined && !options.apiKey.trim())
|
|
44
|
+
throw new Error("--api-key cannot be empty.");
|
|
45
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
46
|
+
if (options.apiKey !== undefined) {
|
|
47
|
+
const apiKey = options.apiKey.trim();
|
|
48
|
+
await setApiKey(runtime, apiKey);
|
|
49
|
+
}
|
|
50
|
+
else if (options.pair === true) {
|
|
51
|
+
await authPair(runtime);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
const beforeAuth = await getAuthState(runtime);
|
|
56
|
+
if (beforeAuth.status !== AuthStatus.SIGNED_IN) {
|
|
57
|
+
const { url } = await beginSignIn(runtime);
|
|
58
|
+
if (options.open === false)
|
|
59
|
+
console.log(`Open this URL to sign in: ${url}`);
|
|
60
|
+
else {
|
|
61
|
+
await openBrowser(url);
|
|
62
|
+
console.log("Opened browser sign-in.");
|
|
63
|
+
}
|
|
64
|
+
await waitForSignIn(runtime, beforeAuth);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
let auth = await getAuthState(runtime);
|
|
68
|
+
if (auth.user?.email)
|
|
69
|
+
console.log(`Signed in as ${auth.user.email}.`);
|
|
70
|
+
// The billing refresh is fired async on install; give it a bounded
|
|
71
|
+
// moment to land so login can report the plan. Silence is fine if the
|
|
72
|
+
// fetch is slow - `nessie-daemon status` shows it later.
|
|
73
|
+
const deadline = Date.now() + 3000;
|
|
74
|
+
while (!auth.billing?.tier && Date.now() < deadline) {
|
|
75
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
76
|
+
auth = await getAuthState(runtime);
|
|
77
|
+
}
|
|
78
|
+
for (const line of billingLines(auth.billing))
|
|
79
|
+
console.log(line);
|
|
80
|
+
}
|
|
81
|
+
async function authPair(runtime) {
|
|
82
|
+
const pairing = await startDevicePairing(runtime, hostname());
|
|
83
|
+
console.log(`Enter code ${pairing.userCode} in Nessie.`);
|
|
84
|
+
if (pairing.verificationHint)
|
|
85
|
+
console.log(pairing.verificationHint);
|
|
86
|
+
if (pairing.expiresAt)
|
|
87
|
+
console.log(`Expires at ${pairing.expiresAt}.`);
|
|
88
|
+
}
|
|
89
|
+
async function authSwitch(userId) {
|
|
90
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
91
|
+
const auth = await switchAccount(runtime, userId);
|
|
92
|
+
if (auth.user?.email)
|
|
93
|
+
console.log(`Switched to ${auth.user.email}.`);
|
|
94
|
+
}
|
|
95
|
+
async function authLogout(userId) {
|
|
96
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
97
|
+
const auth = await signOut(runtime, userId);
|
|
98
|
+
if (auth.user?.email)
|
|
99
|
+
console.log(`Signed in as ${auth.user.email}.`);
|
|
100
|
+
else
|
|
101
|
+
console.log("Signed out.");
|
|
102
|
+
}
|
|
103
|
+
async function waitForSignIn(runtime, beforeAuth) {
|
|
104
|
+
const beforeAccountIds = new Set((beforeAuth.accounts ?? []).map((account) => account.id));
|
|
105
|
+
const alreadySignedIn = beforeAuth.status === AuthStatus.SIGNED_IN || beforeAuth.status === AuthStatus.REAUTH_REQUIRED;
|
|
106
|
+
const deadline = Date.now() + 5 * 60 * 1000;
|
|
107
|
+
while (Date.now() < deadline) {
|
|
108
|
+
const state = await getAuthState(runtime);
|
|
109
|
+
const signedIn = state.status === AuthStatus.SIGNED_IN || state.status === AuthStatus.REAUTH_REQUIRED;
|
|
110
|
+
const hasNewAccount = (state.accounts ?? []).some((account) => !beforeAccountIds.has(account.id));
|
|
111
|
+
if (signedIn && (!alreadySignedIn || hasNewAccount || state.user?.id !== beforeAuth.user?.id || state.status !== beforeAuth.status))
|
|
112
|
+
return;
|
|
113
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
114
|
+
}
|
|
115
|
+
throw new Error("Timed out waiting for browser sign-in.");
|
|
116
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { addSource, configureSource, discoverSources, listSources, removeSource, startIngestion, stopIngestion, triggerIngestion } from "../nera/client.js";
|
|
2
|
+
import { ensureRuntime } from "../nera/process.js";
|
|
3
|
+
import { renderTabTable } from "../platform/table.js";
|
|
4
|
+
export function registerIntegrationCommands(program) {
|
|
5
|
+
const integrations = program.command("integrations").description("Manage local data integrations.");
|
|
6
|
+
integrations.command("list").alias("ls").description("List configured integrations.").action(listIntegrations);
|
|
7
|
+
integrations.command("discover").description("Discover available local integrations.").action(discoverIntegrations);
|
|
8
|
+
integrations
|
|
9
|
+
.command("add <kind>")
|
|
10
|
+
.description("Add an integration.")
|
|
11
|
+
.option("--path <path>", "Override the default local path.")
|
|
12
|
+
.option("--disabled", "Add without enabling automatic ingestion.")
|
|
13
|
+
.action(addIntegration);
|
|
14
|
+
integrations.command("enable <source-id>").description("Enable automatic ingestion for an integration.").action((sourceId) => setEnabled(sourceId, true));
|
|
15
|
+
integrations.command("disable <source-id>").description("Disable automatic ingestion for an integration.").action((sourceId) => setEnabled(sourceId, false));
|
|
16
|
+
integrations.command("remove <source-id>").description("Remove an integration.").action(removeIntegration);
|
|
17
|
+
integrations.command("run").description("Run ingestion once.").action(runIngestion);
|
|
18
|
+
integrations.command("start").description("Enable automatic ingestion for all integrations.").action(startAutomaticIngestion);
|
|
19
|
+
integrations.command("stop").description("Disable automatic ingestion for all integrations.").action(stopAutomaticIngestion);
|
|
20
|
+
}
|
|
21
|
+
async function listIntegrations() {
|
|
22
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
23
|
+
const { sources } = await listSources(runtime);
|
|
24
|
+
if (sources.length === 0) {
|
|
25
|
+
console.log("No integrations configured.");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const rows = [
|
|
29
|
+
["ID", "NAME", "STATUS", "PATH"],
|
|
30
|
+
...sources.map((source) => [source.sourceId, source.displayName, source.enabled ? "enabled" : "disabled", source.basePath]),
|
|
31
|
+
];
|
|
32
|
+
const colors = [
|
|
33
|
+
["bold", "bold", "bold", "bold"],
|
|
34
|
+
...sources.map((source) => ["dim", undefined, source.enabled ? "green" : "yellow", "cyan"]),
|
|
35
|
+
];
|
|
36
|
+
process.stdout.write(renderTabTable(rows, colors));
|
|
37
|
+
}
|
|
38
|
+
async function discoverIntegrations() {
|
|
39
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
40
|
+
const { sources } = await discoverSources(runtime);
|
|
41
|
+
if (sources.length === 0) {
|
|
42
|
+
console.log("No local integrations found.");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const rows = [
|
|
46
|
+
["KIND", "NAME", "STATUS", "PATH"],
|
|
47
|
+
...sources.map((source) => [source.kind, source.displayName, source.alreadyAdded ? "configured" : "available", source.basePath]),
|
|
48
|
+
];
|
|
49
|
+
const colors = [
|
|
50
|
+
["bold", "bold", "bold", "bold"],
|
|
51
|
+
...sources.map((source) => ["dim", undefined, source.alreadyAdded ? "green" : "yellow", "cyan"]),
|
|
52
|
+
];
|
|
53
|
+
process.stdout.write(renderTabTable(rows, colors));
|
|
54
|
+
}
|
|
55
|
+
async function addIntegration(kind, options) {
|
|
56
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
57
|
+
const source = await addSource(runtime, kind, options.path ?? "", options.disabled !== true, false);
|
|
58
|
+
console.log(`Added ${source.displayName}.`);
|
|
59
|
+
}
|
|
60
|
+
async function setEnabled(sourceId, enabled) {
|
|
61
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
62
|
+
const source = await configureSource(runtime, sourceId, enabled);
|
|
63
|
+
console.log(`${enabled ? "Enabled" : "Disabled"} ${source.displayName}.`);
|
|
64
|
+
}
|
|
65
|
+
async function removeIntegration(sourceId) {
|
|
66
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
67
|
+
await removeSource(runtime, sourceId);
|
|
68
|
+
console.log("Removed integration.");
|
|
69
|
+
}
|
|
70
|
+
async function runIngestion() {
|
|
71
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
72
|
+
const result = await triggerIngestion(runtime);
|
|
73
|
+
console.log(`Ran ingestion for ${result.sourceIds.length} integration(s).`);
|
|
74
|
+
}
|
|
75
|
+
async function startAutomaticIngestion() {
|
|
76
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
77
|
+
await startIngestion(runtime);
|
|
78
|
+
console.log("Automatic ingestion enabled.");
|
|
79
|
+
}
|
|
80
|
+
async function stopAutomaticIngestion() {
|
|
81
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
82
|
+
await stopIngestion(runtime);
|
|
83
|
+
console.log("Automatic ingestion disabled.");
|
|
84
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { checkbox, confirm } from "@inquirer/prompts";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { addSource, AuthStatus, beginSignIn, discoverSources, getAuthState, listSources, listTeamInvites, respondToTeamInvite, setApiKey, startCloudSync, startIngestion } from "../nera/client.js";
|
|
4
|
+
import { ensureRuntime } from "../nera/process.js";
|
|
5
|
+
import { openBrowser } from "../platform/openBrowser.js";
|
|
6
|
+
import { service } from "./service.js";
|
|
7
|
+
import { enableCloudSyncForTeam } from "./teams.js";
|
|
8
|
+
import { applyTeamPoliciesForRuntime } from "./sharing.js";
|
|
9
|
+
export async function login(options) {
|
|
10
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
11
|
+
if (runtime.started)
|
|
12
|
+
console.log("Started Nessie.");
|
|
13
|
+
const nonInteractive = options.apiKey !== undefined;
|
|
14
|
+
if (options.apiKey !== undefined) {
|
|
15
|
+
const apiKey = options.apiKey.trim();
|
|
16
|
+
if (!apiKey)
|
|
17
|
+
throw new Error("--api-key cannot be empty.");
|
|
18
|
+
await setApiKey(runtime, apiKey);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const beforeAuth = await getAuthState(runtime);
|
|
22
|
+
if (beforeAuth.status !== AuthStatus.SIGNED_IN) {
|
|
23
|
+
const { url } = await beginSignIn(runtime);
|
|
24
|
+
if (options.open === false) {
|
|
25
|
+
console.log(`Open this URL to sign in: ${url}`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
await openBrowser(url);
|
|
29
|
+
console.log("Opened browser sign-in.");
|
|
30
|
+
}
|
|
31
|
+
await waitForSignIn(runtime, beforeAuth);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const auth = await getAuthState(runtime);
|
|
35
|
+
if (auth.status !== AuthStatus.SIGNED_IN && auth.status !== AuthStatus.REAUTH_REQUIRED) {
|
|
36
|
+
throw new Error("Sign-in did not complete.");
|
|
37
|
+
}
|
|
38
|
+
if (auth.user?.email)
|
|
39
|
+
console.log(`Signed in as ${auth.user.email}.`);
|
|
40
|
+
if (!nonInteractive)
|
|
41
|
+
await handleTeamInvites(runtime);
|
|
42
|
+
await configureSources(runtime, { nonInteractive });
|
|
43
|
+
await startIngestion(runtime);
|
|
44
|
+
let cloudSyncStarted = false;
|
|
45
|
+
try {
|
|
46
|
+
await startCloudSync(runtime);
|
|
47
|
+
cloudSyncStarted = true;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.log(`Cloud sync was not started: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
|
+
}
|
|
52
|
+
if (!nonInteractive && cloudSyncStarted) {
|
|
53
|
+
try {
|
|
54
|
+
await applyTeamPoliciesForRuntime(runtime, { quietNoop: true, retrySyncMetadata: true });
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
console.log("Team sharing policies were not applied yet. Run `nessie-daemon sharing apply-team-policies` to try again.");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const backgroundStarted = options.background !== false;
|
|
61
|
+
if (backgroundStarted) {
|
|
62
|
+
await service("start", { quiet: true });
|
|
63
|
+
}
|
|
64
|
+
console.log(successLine(backgroundStarted
|
|
65
|
+
? "🎉 Nessie is set up successfully. It will run in the background to sync your integrations."
|
|
66
|
+
: "🎉 Nessie is set up successfully."));
|
|
67
|
+
}
|
|
68
|
+
function successLine(message) {
|
|
69
|
+
return process.stdout.isTTY ? `\x1b[32m${message}\x1b[0m` : message;
|
|
70
|
+
}
|
|
71
|
+
async function waitForSignIn(runtime, beforeAuth) {
|
|
72
|
+
const beforeAccountIds = new Set((beforeAuth.accounts ?? []).map((account) => account.id));
|
|
73
|
+
const alreadySignedIn = beforeAuth.status === AuthStatus.SIGNED_IN || beforeAuth.status === AuthStatus.REAUTH_REQUIRED;
|
|
74
|
+
const deadline = Date.now() + 5 * 60 * 1000;
|
|
75
|
+
while (Date.now() < deadline) {
|
|
76
|
+
const state = await getAuthState(runtime);
|
|
77
|
+
const signedIn = state.status === AuthStatus.SIGNED_IN || state.status === AuthStatus.REAUTH_REQUIRED;
|
|
78
|
+
const hasNewAccount = (state.accounts ?? []).some((account) => !beforeAccountIds.has(account.id));
|
|
79
|
+
if (signedIn && (!alreadySignedIn || hasNewAccount || state.user?.id !== beforeAuth.user?.id || state.status !== beforeAuth.status))
|
|
80
|
+
return;
|
|
81
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
82
|
+
}
|
|
83
|
+
throw new Error("Timed out waiting for browser sign-in.");
|
|
84
|
+
}
|
|
85
|
+
async function handleTeamInvites(runtime) {
|
|
86
|
+
const { invites } = await listTeamInvites(runtime);
|
|
87
|
+
let joinedTeamId;
|
|
88
|
+
for (const invite of invites) {
|
|
89
|
+
const accepted = await confirm({
|
|
90
|
+
message: `Join ${invite.teamName} on Nessie?`,
|
|
91
|
+
default: true,
|
|
92
|
+
});
|
|
93
|
+
await respondToTeamInvite(runtime, invite.teamId, accepted);
|
|
94
|
+
console.log(accepted ? `Joined ${invite.teamName}.` : `Declined ${invite.teamName}.`);
|
|
95
|
+
if (accepted)
|
|
96
|
+
joinedTeamId = invite.teamId;
|
|
97
|
+
}
|
|
98
|
+
// Teams share through Nessie Cloud, so joining one opts the account in;
|
|
99
|
+
// without this a daemon-only member can never sync (the account flag is
|
|
100
|
+
// latched by a first preflight the daemon otherwise refuses to run).
|
|
101
|
+
if (joinedTeamId !== undefined)
|
|
102
|
+
await enableCloudSyncForTeam(runtime, joinedTeamId);
|
|
103
|
+
}
|
|
104
|
+
async function configureSources(runtime, options) {
|
|
105
|
+
const [{ sources }, existingSources] = await Promise.all([discoverSources(runtime), listSources(runtime)]);
|
|
106
|
+
const enabledSources = new Set(existingSources.sources.filter((source) => source.enabled).map((source) => `${source.kind}:${source.basePath}`));
|
|
107
|
+
if (sources.length === 0) {
|
|
108
|
+
console.log("No supported local data integrations were found.");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const selected = options.nonInteractive
|
|
112
|
+
? sources
|
|
113
|
+
: await selectSources(sources, enabledSources);
|
|
114
|
+
// Already-enabled sources are a no-op; re-asserting them costs one RPC
|
|
115
|
+
// each, which the ingest-all fast path would otherwise pay on every re-run.
|
|
116
|
+
const changed = selected.filter((source) => !enabledSources.has(`${source.kind}:${source.basePath}`));
|
|
117
|
+
for (const source of changed) {
|
|
118
|
+
await addSource(runtime, source.kind, source.basePath, true, source.alreadyAdded);
|
|
119
|
+
}
|
|
120
|
+
const alreadyEnabled = selected.length - changed.length;
|
|
121
|
+
console.log(alreadyEnabled > 0
|
|
122
|
+
? `Enabled ${changed.length} integration(s) (${alreadyEnabled} already enabled).`
|
|
123
|
+
: `Enabled ${changed.length} integration(s).`);
|
|
124
|
+
}
|
|
125
|
+
/** Fast path first: ingest everything detected; the checkbox is opt-in friction. */
|
|
126
|
+
async function selectSources(sources, enabledSources) {
|
|
127
|
+
for (const source of sources) {
|
|
128
|
+
console.log(` - ${source.displayName} (${source.basePath})`);
|
|
129
|
+
}
|
|
130
|
+
const ingestAll = await confirm({
|
|
131
|
+
message: `Ingest all ${sources.length} detected integration(s) from ${hostname()}?`,
|
|
132
|
+
default: true,
|
|
133
|
+
});
|
|
134
|
+
if (ingestAll)
|
|
135
|
+
return sources;
|
|
136
|
+
return checkbox({
|
|
137
|
+
message: "Choose integrations to ingest:",
|
|
138
|
+
choices: sources.map((source) => ({
|
|
139
|
+
name: `${source.displayName} (${source.basePath})`,
|
|
140
|
+
value: source,
|
|
141
|
+
checked: enabledSources.has(`${source.kind}:${source.basePath}`),
|
|
142
|
+
})),
|
|
143
|
+
required: true,
|
|
144
|
+
});
|
|
145
|
+
}
|
package/dist/commands/run.js
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { ensureRuntime } from "../nera/process.js";
|
|
2
|
+
import { startCloudSync, startIngestion, triggerCloudSync, triggerIngestion } from "../nera/client.js";
|
|
3
|
+
export async function run(options) {
|
|
3
4
|
if (options.once) {
|
|
4
|
-
const
|
|
5
|
-
|
|
5
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
6
|
+
const result = await triggerIngestion(runtime);
|
|
7
|
+
const cloudSyncQueued = await tryCloudSync(() => triggerCloudSync(runtime));
|
|
8
|
+
console.log(`Triggered Nessie ingestion for ${result.sourceIds.length} integration(s)${cloudSyncQueued ? " and queued cloud sync" : ""}.`);
|
|
6
9
|
return;
|
|
7
10
|
}
|
|
8
|
-
const
|
|
9
|
-
await
|
|
11
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
12
|
+
await startIngestion(runtime);
|
|
13
|
+
await tryCloudSync(() => startCloudSync(runtime));
|
|
14
|
+
console.log(runtime.started ? "Started Nessie." : "Nessie is already running.");
|
|
15
|
+
}
|
|
16
|
+
async function tryCloudSync(action) {
|
|
17
|
+
try {
|
|
18
|
+
await action();
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.log(`Cloud sync was not started: ${error instanceof Error ? error.message : String(error)}`);
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
10
25
|
}
|
package/dist/commands/service.js
CHANGED
|
@@ -1,45 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
readConfig,
|
|
10
|
-
};
|
|
11
|
-
export async function runServiceCommand(command, options = {}, depsOverride = {}) {
|
|
12
|
-
const deps = { ...defaultServiceDeps, ...depsOverride };
|
|
13
|
-
if (command === "start" && await shouldStartBackground(options, deps)) {
|
|
14
|
-
console.log(await deps.startBackgroundDaemon());
|
|
15
|
-
return;
|
|
1
|
+
import { startCloudSync, startIngestion } from "../nera/client.js";
|
|
2
|
+
import { ensureRuntime, probeRuntime, stopRuntime } from "../nera/process.js";
|
|
3
|
+
export async function service(command, options = {}) {
|
|
4
|
+
if (command === "start") {
|
|
5
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
6
|
+
await startIngestion(runtime);
|
|
7
|
+
await tryCloudSync(() => startCloudSync(runtime), options.quiet);
|
|
8
|
+
return "";
|
|
16
9
|
}
|
|
17
10
|
if (command === "stop") {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
console.log(await deps.stopBackgroundDaemon());
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
11
|
+
const stopped = await stopRuntime();
|
|
12
|
+
return stopped ? "Stopped Nessie." : "Nessie is not running.";
|
|
23
13
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (status !== null) {
|
|
27
|
-
console.log(status);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const output = await deps.systemctl(command, "nessie-daemon.service");
|
|
32
|
-
if (output.trim())
|
|
33
|
-
console.log(output.trim());
|
|
14
|
+
const runtime = await probeRuntime();
|
|
15
|
+
return runtime ? "Nessie is running." : "Nessie is not running.";
|
|
34
16
|
}
|
|
35
|
-
async function
|
|
36
|
-
if (options.background)
|
|
37
|
-
return true;
|
|
17
|
+
async function tryCloudSync(action, quiet) {
|
|
38
18
|
try {
|
|
39
|
-
|
|
40
|
-
return
|
|
19
|
+
await action();
|
|
20
|
+
return true;
|
|
41
21
|
}
|
|
42
|
-
catch {
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (!quiet)
|
|
24
|
+
console.log(`Cloud sync was not started: ${error instanceof Error ? error.message : String(error)}`);
|
|
43
25
|
return false;
|
|
44
26
|
}
|
|
45
27
|
}
|