@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
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { confirm } from "@inquirer/prompts";
|
|
2
|
+
import { getNodeSharing, listSources, listTeams, listTeamSharingPolicies, NeraRpcError, shareNode, triggerCloudSync, unshareNode, } from "../nera/client.js";
|
|
3
|
+
import { ensureRuntime } from "../nera/process.js";
|
|
4
|
+
import { renderTabTable } from "../platform/table.js";
|
|
5
|
+
var SharingAudience;
|
|
6
|
+
(function (SharingAudience) {
|
|
7
|
+
SharingAudience[SharingAudience["TEAM"] = 1] = "TEAM";
|
|
8
|
+
SharingAudience[SharingAudience["TEAM_ADMINS"] = 2] = "TEAM_ADMINS";
|
|
9
|
+
SharingAudience[SharingAudience["USER"] = 3] = "USER";
|
|
10
|
+
})(SharingAudience || (SharingAudience = {}));
|
|
11
|
+
var SharingScope;
|
|
12
|
+
(function (SharingScope) {
|
|
13
|
+
SharingScope[SharingScope["ALL"] = 1] = "ALL";
|
|
14
|
+
SharingScope[SharingScope["REPOS"] = 2] = "REPOS";
|
|
15
|
+
})(SharingScope || (SharingScope = {}));
|
|
16
|
+
const AUDIENCES = {
|
|
17
|
+
team: SharingAudience.TEAM,
|
|
18
|
+
admins: SharingAudience.TEAM_ADMINS,
|
|
19
|
+
user: SharingAudience.USER,
|
|
20
|
+
};
|
|
21
|
+
const SCOPES = {
|
|
22
|
+
all: SharingScope.ALL,
|
|
23
|
+
repos: SharingScope.REPOS,
|
|
24
|
+
};
|
|
25
|
+
export function registerSharingCommands(program) {
|
|
26
|
+
const sharing = program.command("sharing").description("Manage sharing for synced agent traces.");
|
|
27
|
+
sharing.command("get <node-id>").description("Show sharing for a node.").action(showSharing);
|
|
28
|
+
sharing
|
|
29
|
+
.command("add <node-id>")
|
|
30
|
+
.description("Share a node.")
|
|
31
|
+
.requiredOption("--team <team-id>", "Team id.")
|
|
32
|
+
.option("--audience <audience>", "team, admins, or user.", "team")
|
|
33
|
+
.option("--user <user-id>", "User id for user-scoped sharing.", collect, [])
|
|
34
|
+
.option("--scope <scope>", "all or repos.", "all")
|
|
35
|
+
.option("--repo <repo-key>", "Repo key for repo-scoped sharing.", collect, [])
|
|
36
|
+
.action(addSharing);
|
|
37
|
+
sharing
|
|
38
|
+
.command("remove <node-id>")
|
|
39
|
+
.description("Remove sharing from a node.")
|
|
40
|
+
.option("--team <team-id>", "Team id.")
|
|
41
|
+
.option("--user <user-id>", "User id for per-user sharing.", collect, [])
|
|
42
|
+
.action(removeSharing);
|
|
43
|
+
sharing
|
|
44
|
+
.command("apply-team-policies")
|
|
45
|
+
.description("Apply team sharing policy suggestions to configured integrations.")
|
|
46
|
+
.action(applyTeamPolicies);
|
|
47
|
+
}
|
|
48
|
+
async function showSharing(nodeId) {
|
|
49
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
50
|
+
printSharing(await getNodeSharing(runtime, nodeId));
|
|
51
|
+
}
|
|
52
|
+
async function addSharing(nodeId, options) {
|
|
53
|
+
const request = shareRequest(nodeId, options);
|
|
54
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
55
|
+
const sharing = await shareNode(runtime, request);
|
|
56
|
+
printSharing(sharing);
|
|
57
|
+
}
|
|
58
|
+
async function removeSharing(nodeId, options) {
|
|
59
|
+
if (!options.team && (options.user ?? []).length === 0) {
|
|
60
|
+
throw new Error("sharing remove requires --team or --user.");
|
|
61
|
+
}
|
|
62
|
+
if (!options.team && (options.user ?? []).length > 0) {
|
|
63
|
+
throw new Error("--user can only be used with --team.");
|
|
64
|
+
}
|
|
65
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
66
|
+
const sharing = await unshareNode(runtime, { nodeId, teamId: options.team, userIds: options.user });
|
|
67
|
+
printSharing(sharing);
|
|
68
|
+
}
|
|
69
|
+
async function applyTeamPolicies() {
|
|
70
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
71
|
+
await applyTeamPoliciesForRuntime(runtime, { retrySyncMetadata: true });
|
|
72
|
+
}
|
|
73
|
+
export async function applyTeamPoliciesForRuntime(runtime, options = {}) {
|
|
74
|
+
const [{ sources }, { teams }] = await Promise.all([listSources(runtime), listTeams(runtime)]);
|
|
75
|
+
if (teams.length === 0) {
|
|
76
|
+
if (!options.quietNoop)
|
|
77
|
+
console.log("No teams.");
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
const bundles = await listTeamPolicyBundles(runtime, teams);
|
|
81
|
+
const plan = buildTeamPolicyPlan(sources, bundles);
|
|
82
|
+
if (plan.length === 0) {
|
|
83
|
+
if (!options.quietNoop)
|
|
84
|
+
console.log("No enabled integrations match current team sharing policies.");
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
const changes = await planTeamPolicyChanges(runtime, plan);
|
|
88
|
+
if (changes.length === 0) {
|
|
89
|
+
if (!options.quietNoop)
|
|
90
|
+
console.log("Team sharing policies are already applied.");
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
printTeamPolicyPlan(changes);
|
|
94
|
+
const approved = await confirm({
|
|
95
|
+
message: `Apply ${changes.length} sharing change(s)?`,
|
|
96
|
+
default: true,
|
|
97
|
+
});
|
|
98
|
+
if (!approved) {
|
|
99
|
+
console.log("No sharing changes applied.");
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
await applyPlannedShares(runtime, changes, options);
|
|
103
|
+
return changes.length;
|
|
104
|
+
}
|
|
105
|
+
async function applyPlannedShares(runtime, plan, options) {
|
|
106
|
+
const failures = [];
|
|
107
|
+
let applied = 0;
|
|
108
|
+
for (const item of plan) {
|
|
109
|
+
try {
|
|
110
|
+
await shareWithRetry(runtime, item, options);
|
|
111
|
+
applied += 1;
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
failures.push({ item, error });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (failures.length === 0) {
|
|
118
|
+
console.log(`Applied ${applied} sharing change(s).`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
console.error(`Applied ${applied} of ${plan.length} sharing change(s); ${failures.length} failed.`);
|
|
122
|
+
for (const { item, error } of failures) {
|
|
123
|
+
console.error(`${item.teamName} / ${item.sourceName} / ${formatAudience(item.audience, item.userIds[0])}: ${errorMessage(error)}`);
|
|
124
|
+
}
|
|
125
|
+
throw new Error("Some sharing changes failed.");
|
|
126
|
+
}
|
|
127
|
+
async function shareWithRetry(runtime, item, options) {
|
|
128
|
+
const maxAttempts = options.retrySyncMetadata ? 7 : 1;
|
|
129
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
130
|
+
try {
|
|
131
|
+
await shareNode(runtime, {
|
|
132
|
+
nodeId: item.sourceId,
|
|
133
|
+
teamId: item.teamId,
|
|
134
|
+
audience: item.audience,
|
|
135
|
+
userIds: item.userIds,
|
|
136
|
+
scope: item.scope,
|
|
137
|
+
repoKeys: item.repoKeys,
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (attempt >= maxAttempts || !isSyncMetadataPendingError(error))
|
|
143
|
+
throw error;
|
|
144
|
+
if (attempt === 1)
|
|
145
|
+
console.log("Waiting for Nessie to finish preparing sharing metadata...");
|
|
146
|
+
try {
|
|
147
|
+
await triggerCloudSync(runtime);
|
|
148
|
+
}
|
|
149
|
+
catch (syncError) {
|
|
150
|
+
console.warn(`Could not trigger cloud sync while waiting for sharing metadata: ${errorMessage(syncError)}`);
|
|
151
|
+
}
|
|
152
|
+
await sleep(5000);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function isSyncMetadataPendingError(error) {
|
|
157
|
+
return error instanceof NeraRpcError && error.nessieError === "sync_resource_pending";
|
|
158
|
+
}
|
|
159
|
+
function sleep(ms) {
|
|
160
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
161
|
+
}
|
|
162
|
+
function errorMessage(error) {
|
|
163
|
+
return error instanceof Error ? error.message : String(error);
|
|
164
|
+
}
|
|
165
|
+
function printSharing(sharing) {
|
|
166
|
+
if (sharing.grants.length === 0) {
|
|
167
|
+
console.log("Not shared.");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const rows = [
|
|
171
|
+
["TEAM", "AUDIENCE", "SCOPE", "ORIGIN"],
|
|
172
|
+
...sharing.grants.map((grant) => [
|
|
173
|
+
grant.teamId,
|
|
174
|
+
formatAudience(grant.audience, grant.userId),
|
|
175
|
+
grant.scope === SharingScope.REPOS ? `repos:${(grant.repoKeys ?? []).join(",")}` : "all",
|
|
176
|
+
grant.inherited ? `inherited:${grant.inheritedFromNodeId ?? ""}` : "direct",
|
|
177
|
+
]),
|
|
178
|
+
];
|
|
179
|
+
const colors = [
|
|
180
|
+
["bold", "bold", "bold", "bold"],
|
|
181
|
+
...sharing.grants.map((grant) => ["dim", grant.audience === SharingAudience.TEAM ? "green" : "cyan", undefined, grant.inherited ? "yellow" : "green"]),
|
|
182
|
+
];
|
|
183
|
+
process.stdout.write(renderTabTable(rows, colors));
|
|
184
|
+
}
|
|
185
|
+
function collect(value, previous) {
|
|
186
|
+
previous.push(value);
|
|
187
|
+
return previous;
|
|
188
|
+
}
|
|
189
|
+
export function buildTeamPolicyPlan(sources, bundles) {
|
|
190
|
+
const enabledSources = sources.filter((source) => source.enabled);
|
|
191
|
+
const byKey = new Map();
|
|
192
|
+
for (const { team, policies } of bundles) {
|
|
193
|
+
for (const policy of policies) {
|
|
194
|
+
if (!policy.enabled)
|
|
195
|
+
continue;
|
|
196
|
+
const kinds = new Set(policy.resourceKinds.map((kind) => kind.trim().toLowerCase()).filter(Boolean));
|
|
197
|
+
if (kinds.size === 0)
|
|
198
|
+
continue;
|
|
199
|
+
const matchingSources = enabledSources.filter((source) => kinds.has(source.kind.trim().toLowerCase()));
|
|
200
|
+
if (matchingSources.length === 0)
|
|
201
|
+
continue;
|
|
202
|
+
const scope = policyScope(policy);
|
|
203
|
+
if (scope === undefined)
|
|
204
|
+
continue;
|
|
205
|
+
const repoKeys = scope === SharingScope.REPOS ? normalizeList(policy.repoKeys) : [];
|
|
206
|
+
if (scope === SharingScope.REPOS && repoKeys.length === 0)
|
|
207
|
+
continue;
|
|
208
|
+
const targets = policyTargets(policy);
|
|
209
|
+
if (targets.length === 0)
|
|
210
|
+
continue;
|
|
211
|
+
for (const source of matchingSources) {
|
|
212
|
+
for (const target of targets) {
|
|
213
|
+
const key = [source.sourceId, team.teamId, target.audience, target.userId].join(":");
|
|
214
|
+
const existing = byKey.get(key);
|
|
215
|
+
if (!existing) {
|
|
216
|
+
byKey.set(key, {
|
|
217
|
+
sourceId: source.sourceId,
|
|
218
|
+
sourceName: source.displayName,
|
|
219
|
+
sourceKind: source.kind,
|
|
220
|
+
teamId: team.teamId,
|
|
221
|
+
teamName: team.name,
|
|
222
|
+
audience: target.audience,
|
|
223
|
+
userIds: target.userId ? [target.userId] : [],
|
|
224
|
+
scope,
|
|
225
|
+
repoKeys,
|
|
226
|
+
policyNames: [policy.name],
|
|
227
|
+
});
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
existing.policyNames = normalizeList([...existing.policyNames, policy.name]);
|
|
231
|
+
if (existing.scope === SharingScope.ALL || scope === SharingScope.ALL) {
|
|
232
|
+
existing.scope = SharingScope.ALL;
|
|
233
|
+
existing.repoKeys = [];
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
existing.repoKeys = normalizeList([...existing.repoKeys, ...repoKeys]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return Array.from(byKey.values()).sort((a, b) => a.teamName.localeCompare(b.teamName)
|
|
243
|
+
|| a.sourceName.localeCompare(b.sourceName)
|
|
244
|
+
|| formatAudience(a.audience, a.userIds[0]).localeCompare(formatAudience(b.audience, b.userIds[0])));
|
|
245
|
+
}
|
|
246
|
+
export async function listTeamPolicyBundles(runtime, teams) {
|
|
247
|
+
const results = await Promise.allSettled(teams.map(async (team) => ({
|
|
248
|
+
team,
|
|
249
|
+
policies: (await listTeamSharingPolicies(runtime, team.teamId)).policies,
|
|
250
|
+
})));
|
|
251
|
+
const bundles = [];
|
|
252
|
+
for (let index = 0; index < results.length; index += 1) {
|
|
253
|
+
const result = results[index];
|
|
254
|
+
if (result?.status === "fulfilled") {
|
|
255
|
+
bundles.push(result.value);
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
const team = teams[index];
|
|
259
|
+
console.warn(`Skipping team sharing policies for ${team?.name ?? "unknown team"}: ${errorMessage(result?.reason)}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return bundles;
|
|
263
|
+
}
|
|
264
|
+
function policyScope(policy) {
|
|
265
|
+
const scope = policy.shareScope.trim().toLowerCase();
|
|
266
|
+
if (scope === "" || scope === "all")
|
|
267
|
+
return SharingScope.ALL;
|
|
268
|
+
if (scope === "repos")
|
|
269
|
+
return SharingScope.REPOS;
|
|
270
|
+
console.warn(`Skipping policy "${policy.name}": unknown scope "${policy.shareScope}".`);
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
function policyTargets(policy) {
|
|
274
|
+
switch (policy.audience.trim().toLowerCase()) {
|
|
275
|
+
case "team":
|
|
276
|
+
return [{ audience: SharingAudience.TEAM }];
|
|
277
|
+
case "admins":
|
|
278
|
+
return [{ audience: SharingAudience.TEAM_ADMINS }];
|
|
279
|
+
case "user":
|
|
280
|
+
case "users":
|
|
281
|
+
return normalizeList(policy.audienceUserIds).map((userId) => ({ audience: SharingAudience.USER, userId }));
|
|
282
|
+
default:
|
|
283
|
+
console.warn(`Skipping policy "${policy.name}": unknown audience "${policy.audience}".`);
|
|
284
|
+
return [];
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function normalizeList(values = []) {
|
|
288
|
+
const seen = new Set();
|
|
289
|
+
const result = [];
|
|
290
|
+
for (const value of values) {
|
|
291
|
+
const trimmed = value.trim();
|
|
292
|
+
if (!trimmed || seen.has(trimmed))
|
|
293
|
+
continue;
|
|
294
|
+
seen.add(trimmed);
|
|
295
|
+
result.push(trimmed);
|
|
296
|
+
}
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
export async function planTeamPolicyChanges(runtime, plan) {
|
|
300
|
+
const sourceIds = [...new Set(plan.map((item) => item.sourceId))];
|
|
301
|
+
const sharingBySource = new Map(await Promise.all(sourceIds.map(async (sourceId) => [sourceId, await getNodeSharing(runtime, sourceId)])));
|
|
302
|
+
return diffTeamPolicyPlan(plan, sharingBySource);
|
|
303
|
+
}
|
|
304
|
+
export function diffTeamPolicyPlan(plan, sharingBySource) {
|
|
305
|
+
const changes = [];
|
|
306
|
+
for (const item of plan) {
|
|
307
|
+
const grant = directGrantFor(item, sharingBySource.get(item.sourceId));
|
|
308
|
+
if (!grant) {
|
|
309
|
+
changes.push({ ...item, action: "add" });
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (grant.scope === item.scope && sameSet(grant.repoKeys ?? [], item.repoKeys))
|
|
313
|
+
continue;
|
|
314
|
+
changes.push({ ...item, action: "change" });
|
|
315
|
+
}
|
|
316
|
+
return changes;
|
|
317
|
+
}
|
|
318
|
+
function directGrantFor(item, sharing) {
|
|
319
|
+
return sharing?.grants.find((grant) => !grant.inherited
|
|
320
|
+
&& grant.teamId === item.teamId
|
|
321
|
+
&& grant.audience === item.audience
|
|
322
|
+
&& (item.audience !== SharingAudience.USER || grant.userId === item.userIds[0])
|
|
323
|
+
&& (item.audience === SharingAudience.USER || !grant.userId));
|
|
324
|
+
}
|
|
325
|
+
function sameSet(left, right) {
|
|
326
|
+
const normalizedLeft = normalizeList(left).sort();
|
|
327
|
+
const normalizedRight = normalizeList(right).sort();
|
|
328
|
+
return normalizedLeft.length === normalizedRight.length && normalizedLeft.every((value, index) => value === normalizedRight[index]);
|
|
329
|
+
}
|
|
330
|
+
function printTeamPolicyPlan(plan) {
|
|
331
|
+
console.log("Team sharing policies will apply these changes:");
|
|
332
|
+
const rows = [
|
|
333
|
+
["ACTION", "TEAM", "INTEGRATION", "AUDIENCE", "SCOPE", "POLICY"],
|
|
334
|
+
...plan.map((item) => [
|
|
335
|
+
item.action.toUpperCase(),
|
|
336
|
+
item.teamName,
|
|
337
|
+
item.sourceName,
|
|
338
|
+
formatAudience(item.audience, item.userIds[0]),
|
|
339
|
+
item.scope === SharingScope.REPOS ? `repos:${item.repoKeys.join(",")}` : "all",
|
|
340
|
+
item.policyNames.join(", "),
|
|
341
|
+
]),
|
|
342
|
+
];
|
|
343
|
+
const colors = [
|
|
344
|
+
["bold", "bold", "bold", "bold", "bold", "bold"],
|
|
345
|
+
...plan.map((item) => [item.action === "change" ? "yellow" : "green", "dim", undefined, item.audience === SharingAudience.TEAM ? "green" : "cyan", undefined, "dim"]),
|
|
346
|
+
];
|
|
347
|
+
process.stdout.write(renderTabTable(rows, colors));
|
|
348
|
+
}
|
|
349
|
+
export function parseAudience(value) {
|
|
350
|
+
if (isAudienceInput(value))
|
|
351
|
+
return AUDIENCES[value];
|
|
352
|
+
throw new Error("--audience must be team, admins, or user.");
|
|
353
|
+
}
|
|
354
|
+
export function parseScope(value) {
|
|
355
|
+
if (isScopeInput(value))
|
|
356
|
+
return SCOPES[value];
|
|
357
|
+
throw new Error("--scope must be all or repos.");
|
|
358
|
+
}
|
|
359
|
+
export function formatAudience(audience, userId) {
|
|
360
|
+
if (audience === SharingAudience.TEAM)
|
|
361
|
+
return "team";
|
|
362
|
+
if (audience === SharingAudience.TEAM_ADMINS)
|
|
363
|
+
return "admins";
|
|
364
|
+
if (audience === SharingAudience.USER)
|
|
365
|
+
return `user:${userId ?? ""}`;
|
|
366
|
+
return "unknown";
|
|
367
|
+
}
|
|
368
|
+
export function shareRequest(nodeId, options) {
|
|
369
|
+
const audience = parseAudience(options.audience);
|
|
370
|
+
const scope = parseScope(options.scope);
|
|
371
|
+
const userIds = options.user ?? [];
|
|
372
|
+
const repoKeys = options.repo ?? [];
|
|
373
|
+
if (audience === SharingAudience.USER && userIds.length === 0) {
|
|
374
|
+
throw new Error("--audience user requires at least one --user.");
|
|
375
|
+
}
|
|
376
|
+
if (audience !== SharingAudience.USER && userIds.length > 0) {
|
|
377
|
+
throw new Error("--user can only be used with --audience user.");
|
|
378
|
+
}
|
|
379
|
+
if (scope === SharingScope.REPOS && repoKeys.length === 0) {
|
|
380
|
+
throw new Error("--scope repos requires at least one --repo.");
|
|
381
|
+
}
|
|
382
|
+
if (scope !== SharingScope.REPOS && repoKeys.length > 0) {
|
|
383
|
+
throw new Error("--repo can only be used with --scope repos.");
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
nodeId,
|
|
387
|
+
teamId: options.team,
|
|
388
|
+
audience,
|
|
389
|
+
userIds,
|
|
390
|
+
scope,
|
|
391
|
+
repoKeys,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
function isAudienceInput(value) {
|
|
395
|
+
return Object.hasOwn(AUDIENCES, value);
|
|
396
|
+
}
|
|
397
|
+
function isScopeInput(value) {
|
|
398
|
+
return Object.hasOwn(SCOPES, value);
|
|
399
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { getAuthState, getRuntimeStatus, listSources } from "../nera/client.js";
|
|
2
|
+
import { probeRuntime } from "../nera/process.js";
|
|
3
|
+
export async function status() {
|
|
4
|
+
const runtime = await probeRuntime();
|
|
5
|
+
if (!runtime) {
|
|
6
|
+
console.log("Nessie is not running.");
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
console.log("Nessie is running.");
|
|
10
|
+
try {
|
|
11
|
+
const auth = await getAuthState(runtime);
|
|
12
|
+
if (auth.user?.email)
|
|
13
|
+
console.log(`Signed in as ${auth.user.email}.`);
|
|
14
|
+
for (const line of billingLines(auth.billing))
|
|
15
|
+
console.log(line);
|
|
16
|
+
const { sources } = await listSources(runtime);
|
|
17
|
+
const enabled = sources.filter((source) => source.enabled);
|
|
18
|
+
console.log(`Enabled integrations: ${enabled.length}.`);
|
|
19
|
+
const runtimeStatus = await getRuntimeStatus(runtime);
|
|
20
|
+
for (const line of engineLines("Ingestion", runtimeStatus.ingestion))
|
|
21
|
+
console.log(line);
|
|
22
|
+
for (const line of engineLines("Cloud sync", runtimeStatus.cloudSync))
|
|
23
|
+
console.log(line);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// A healthy runtime without auth is still a useful status.
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Renders the last known billing status. An unset status (not fetched
|
|
30
|
+
* yet, offline) prints nothing - unknown is not "no access". */
|
|
31
|
+
export function billingLines(billing) {
|
|
32
|
+
if (!billing || !billing.tier)
|
|
33
|
+
return [];
|
|
34
|
+
const plan = billing.accessSource === "team_subscription" && billing.teamName
|
|
35
|
+
? `${billing.tier} (via team ${billing.teamName})`
|
|
36
|
+
: billing.tier;
|
|
37
|
+
const lines = [`Plan: ${plan}.`];
|
|
38
|
+
if (!billing.hasAccess) {
|
|
39
|
+
lines.push("No active subscription: Nessie is paused. Join a team or subscribe to resume.");
|
|
40
|
+
}
|
|
41
|
+
return lines;
|
|
42
|
+
}
|
|
43
|
+
export function engineLines(label, status) {
|
|
44
|
+
if (!status)
|
|
45
|
+
return [];
|
|
46
|
+
const state = status.running
|
|
47
|
+
? `running${status.lastTrigger ? ` (${status.lastTrigger})` : ""}`
|
|
48
|
+
: "idle";
|
|
49
|
+
const lines = [`${label}: ${state}. Automatic: ${status.autoEnabled ? "on" : "off"}.`];
|
|
50
|
+
lines.push(`${label} last started: ${timestampOrNever(status.lastStartedAt)}.`);
|
|
51
|
+
lines.push(`${label} last succeeded: ${timestampOrNever(status.lastSuccessAt)}.`);
|
|
52
|
+
if (status.lastSkippedAt) {
|
|
53
|
+
const reason = status.lastSkipReason ? ` (${status.lastSkipReason})` : "";
|
|
54
|
+
lines.push(`${label} last skipped: ${status.lastSkippedAt}${reason}.`);
|
|
55
|
+
}
|
|
56
|
+
if (status.lastCompletedAt &&
|
|
57
|
+
status.lastCompletedAt !== status.lastSuccessAt &&
|
|
58
|
+
status.lastCompletedAt !== status.lastSkippedAt) {
|
|
59
|
+
lines.push(`${label} last completed: ${status.lastCompletedAt}.`);
|
|
60
|
+
}
|
|
61
|
+
if (status.lastError)
|
|
62
|
+
lines.push(`${label} last error: ${status.lastError}.`);
|
|
63
|
+
return lines;
|
|
64
|
+
}
|
|
65
|
+
function timestampOrNever(value) {
|
|
66
|
+
return value && value.length > 0 ? value : "never";
|
|
67
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { enableCloudSync, listTeamInvites, listTeams, respondToTeamInvite } from "../nera/client.js";
|
|
2
|
+
import { ensureRuntime } from "../nera/process.js";
|
|
3
|
+
import { renderTabTable } from "../platform/table.js";
|
|
4
|
+
export function registerTeamCommands(program) {
|
|
5
|
+
const teams = program.command("teams").description("Manage Nessie teams.");
|
|
6
|
+
teams.command("list").alias("ls").description("List teams.").action(teamList);
|
|
7
|
+
teams.command("invites").description("List pending team invites.").action(teamInvites);
|
|
8
|
+
teams.command("accept <team-id>").description("Accept a team invite.").action((teamId) => respond(teamId, true));
|
|
9
|
+
teams.command("decline <team-id>").description("Decline a team invite.").action((teamId) => respond(teamId, false));
|
|
10
|
+
}
|
|
11
|
+
async function teamList() {
|
|
12
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
13
|
+
const { teams } = await listTeams(runtime);
|
|
14
|
+
if (teams.length === 0) {
|
|
15
|
+
console.log("No teams.");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const rows = [
|
|
19
|
+
["NAME", "ROLE", "ID"],
|
|
20
|
+
...teams.map((team) => [team.name, team.role, team.teamId]),
|
|
21
|
+
];
|
|
22
|
+
const colors = [
|
|
23
|
+
["bold", "bold", "bold"],
|
|
24
|
+
...teams.map((team) => [undefined, team.role === "admin" ? "green" : "cyan", "dim"]),
|
|
25
|
+
];
|
|
26
|
+
process.stdout.write(renderTabTable(rows, colors));
|
|
27
|
+
}
|
|
28
|
+
async function teamInvites() {
|
|
29
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
30
|
+
const { invites } = await listTeamInvites(runtime);
|
|
31
|
+
if (invites.length === 0) {
|
|
32
|
+
console.log("No pending invites.");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const rows = [
|
|
36
|
+
["TEAM", "INVITED BY", "CREATED", "ID"],
|
|
37
|
+
...invites.map((invite) => [invite.teamName, invite.invitedByName, invite.createdAt, invite.teamId]),
|
|
38
|
+
];
|
|
39
|
+
const colors = [
|
|
40
|
+
["bold", "bold", "bold", "bold"],
|
|
41
|
+
...invites.map(() => [undefined, undefined, "dim", "dim"]),
|
|
42
|
+
];
|
|
43
|
+
process.stdout.write(renderTabTable(rows, colors));
|
|
44
|
+
}
|
|
45
|
+
async function respond(teamId, accept) {
|
|
46
|
+
const runtime = await ensureRuntime({ detached: true });
|
|
47
|
+
await respondToTeamInvite(runtime, teamId, accept);
|
|
48
|
+
console.log(accept ? "Invite accepted." : "Invite declined.");
|
|
49
|
+
if (accept)
|
|
50
|
+
await enableCloudSyncForTeam(runtime, teamId);
|
|
51
|
+
}
|
|
52
|
+
/** Teams share through Nessie Cloud, so joining one opts the account into cloud sync. */
|
|
53
|
+
export async function enableCloudSyncForTeam(runtime, teamId) {
|
|
54
|
+
try {
|
|
55
|
+
await enableCloudSync(runtime);
|
|
56
|
+
console.log("Cloud sync enabled. Your team shares data through Nessie Cloud.");
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.log(`Cloud sync could not be enabled: ${error instanceof Error ? error.message : String(error)}`);
|
|
60
|
+
// Re-accepting an already-accepted invite is a no-op on the backend but
|
|
61
|
+
// re-fires this enable, so it works as a durable retry.
|
|
62
|
+
console.log(`Team sharing needs cloud sync. Run \`nessie-daemon teams accept ${teamId}\` again to retry.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import * as grpc from "@grpc/grpc-js";
|
|
6
|
+
import * as protoLoader from "@grpc/proto-loader";
|
|
7
|
+
export var AuthStatus;
|
|
8
|
+
(function (AuthStatus) {
|
|
9
|
+
AuthStatus[AuthStatus["SIGNED_IN"] = 3] = "SIGNED_IN";
|
|
10
|
+
AuthStatus[AuthStatus["REAUTH_REQUIRED"] = 4] = "REAUTH_REQUIRED";
|
|
11
|
+
})(AuthStatus || (AuthStatus = {}));
|
|
12
|
+
export class NeraRpcError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
nessieError;
|
|
15
|
+
constructor(message, code, nessieError) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.nessieError = nessieError;
|
|
19
|
+
this.name = "NeraRpcError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const packagedProtoPath = resolve(moduleDir, "../../vendor/proto/nessie.edge.v1.proto");
|
|
24
|
+
const sourceProtoPath = resolve(moduleDir, "../../../nera/proto/nessie.edge.v1.proto");
|
|
25
|
+
const protoPath = existsSync(packagedProtoPath) ? packagedProtoPath : sourceProtoPath;
|
|
26
|
+
const packageDefinition = protoLoader.loadSync(protoPath, {
|
|
27
|
+
keepCase: false,
|
|
28
|
+
longs: String,
|
|
29
|
+
enums: Number,
|
|
30
|
+
defaults: true,
|
|
31
|
+
oneofs: true,
|
|
32
|
+
});
|
|
33
|
+
const proto = grpc.loadPackageDefinition(packageDefinition);
|
|
34
|
+
const NeraClient = proto.nessie.edge.v1.Nera;
|
|
35
|
+
export async function getHealth(endpoint) {
|
|
36
|
+
return call(endpoint, "GetHealth", {});
|
|
37
|
+
}
|
|
38
|
+
export async function beginSignIn(endpoint) {
|
|
39
|
+
return call(endpoint, "BeginSignIn", {});
|
|
40
|
+
}
|
|
41
|
+
export async function setApiKey(endpoint, apiKey) {
|
|
42
|
+
return call(endpoint, "SetApiKey", { apiKey });
|
|
43
|
+
}
|
|
44
|
+
export async function startDevicePairing(endpoint, deviceName) {
|
|
45
|
+
return call(endpoint, "StartDevicePairing", { deviceName });
|
|
46
|
+
}
|
|
47
|
+
export async function switchAccount(endpoint, userId) {
|
|
48
|
+
return call(endpoint, "SwitchAccount", { userId });
|
|
49
|
+
}
|
|
50
|
+
export async function signOut(endpoint, userId) {
|
|
51
|
+
return call(endpoint, "SignOut", { userId: userId ?? "" });
|
|
52
|
+
}
|
|
53
|
+
export async function getAuthState(endpoint) {
|
|
54
|
+
return call(endpoint, "GetAuthState", {});
|
|
55
|
+
}
|
|
56
|
+
export async function getRuntimeStatus(endpoint) {
|
|
57
|
+
return call(endpoint, "GetRuntimeStatus", {});
|
|
58
|
+
}
|
|
59
|
+
export async function listTeams(endpoint) {
|
|
60
|
+
return call(endpoint, "ListTeams", {});
|
|
61
|
+
}
|
|
62
|
+
export async function listTeamInvites(endpoint) {
|
|
63
|
+
return call(endpoint, "ListTeamInvites", {});
|
|
64
|
+
}
|
|
65
|
+
export async function respondToTeamInvite(endpoint, teamId, accept) {
|
|
66
|
+
return call(endpoint, "RespondToTeamInvite", { teamId, accept });
|
|
67
|
+
}
|
|
68
|
+
export async function listTeamSharingPolicies(endpoint, teamId) {
|
|
69
|
+
return call(endpoint, "ListTeamSharingPolicies", { teamId });
|
|
70
|
+
}
|
|
71
|
+
export async function discoverSources(endpoint) {
|
|
72
|
+
return call(endpoint, "DiscoverSources", {});
|
|
73
|
+
}
|
|
74
|
+
export async function listSources(endpoint) {
|
|
75
|
+
return call(endpoint, "ListSources", {});
|
|
76
|
+
}
|
|
77
|
+
export async function addSource(endpoint, kind, basePath, enabled, alreadyAdded) {
|
|
78
|
+
if (alreadyAdded) {
|
|
79
|
+
const { sources } = await listSources(endpoint);
|
|
80
|
+
const existing = sources.find((source) => source.kind === kind && source.basePath === basePath);
|
|
81
|
+
if (!existing)
|
|
82
|
+
throw new Error(`Source ${kind} at ${basePath} was reported as added but could not be listed.`);
|
|
83
|
+
return call(endpoint, "ConfigureSource", { sourceId: existing.sourceId, enabled, basePath });
|
|
84
|
+
}
|
|
85
|
+
return call(endpoint, "AddSource", { kind, basePath, enabled });
|
|
86
|
+
}
|
|
87
|
+
export async function configureSource(endpoint, sourceId, enabled) {
|
|
88
|
+
return call(endpoint, "ConfigureSource", { sourceId, enabled });
|
|
89
|
+
}
|
|
90
|
+
export async function removeSource(endpoint, sourceId) {
|
|
91
|
+
return call(endpoint, "RemoveSource", { sourceId });
|
|
92
|
+
}
|
|
93
|
+
export async function startIngestion(endpoint) {
|
|
94
|
+
return call(endpoint, "StartIngestion", {});
|
|
95
|
+
}
|
|
96
|
+
export async function stopIngestion(endpoint) {
|
|
97
|
+
return call(endpoint, "StopIngestion", {});
|
|
98
|
+
}
|
|
99
|
+
export async function startCloudSync(endpoint) {
|
|
100
|
+
return call(endpoint, "StartCloudSync", {});
|
|
101
|
+
}
|
|
102
|
+
export async function triggerIngestion(endpoint) {
|
|
103
|
+
return call(endpoint, "TriggerIngestion", {});
|
|
104
|
+
}
|
|
105
|
+
export async function triggerCloudSync(endpoint) {
|
|
106
|
+
return call(endpoint, "TriggerCloudSync", {});
|
|
107
|
+
}
|
|
108
|
+
/** Account-level opt-in: runs a sync preflight and turns the automatic loop on. */
|
|
109
|
+
export async function enableCloudSync(endpoint) {
|
|
110
|
+
return call(endpoint, "EnableCloudSync", {});
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Turns the automatic loop off; the account-level flag stays set on the backend.
|
|
114
|
+
* Intentionally has no CLI entrypoint: cloud sync is default-off and opt-in
|
|
115
|
+
* rides team-invite accept only (product decision 2026-07-05), so this wrapper
|
|
116
|
+
* exists for API symmetry and non-CLI consumers of the daemon surface.
|
|
117
|
+
*/
|
|
118
|
+
export async function disableCloudSync(endpoint) {
|
|
119
|
+
return call(endpoint, "DisableCloudSync", {});
|
|
120
|
+
}
|
|
121
|
+
export async function getNodeSharing(endpoint, nodeId) {
|
|
122
|
+
return call(endpoint, "GetNodeSharing", { nodeId });
|
|
123
|
+
}
|
|
124
|
+
export async function shareNode(endpoint, request) {
|
|
125
|
+
return call(endpoint, "ShareNode", request);
|
|
126
|
+
}
|
|
127
|
+
export async function unshareNode(endpoint, request) {
|
|
128
|
+
return call(endpoint, "UnshareNode", { nodeId: request.nodeId, teamId: request.teamId ?? "", userIds: request.userIds ?? [] });
|
|
129
|
+
}
|
|
130
|
+
async function call(endpoint, method, request) {
|
|
131
|
+
const client = new NeraClient(`${endpoint.host}:${endpoint.port}`, grpc.credentials.createInsecure());
|
|
132
|
+
const metadata = new grpc.Metadata();
|
|
133
|
+
metadata.set("authorization", `Bearer ${endpoint.token}`);
|
|
134
|
+
const rpc = promisify(client[method].bind(client));
|
|
135
|
+
try {
|
|
136
|
+
return await rpc(request, metadata);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
if (isServiceError(error)) {
|
|
140
|
+
throw new NeraRpcError(error.details || error.message, error.code, metadataValue(error.metadata, "nessie-error"));
|
|
141
|
+
}
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
client.close();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function isServiceError(error) {
|
|
149
|
+
return typeof error === "object" && error !== null && "code" in error && "metadata" in error;
|
|
150
|
+
}
|
|
151
|
+
function metadataValue(metadata, key) {
|
|
152
|
+
const value = metadata?.get(key)[0];
|
|
153
|
+
if (typeof value === "string")
|
|
154
|
+
return value;
|
|
155
|
+
return value?.toString();
|
|
156
|
+
}
|