@nurix/nustack 0.10.0-dev.10
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 +77 -0
- package/dist/commands/add.js +130 -0
- package/dist/commands/agent.js +100 -0
- package/dist/commands/bootstrap.js +54 -0
- package/dist/commands/describe.js +34 -0
- package/dist/commands/device.js +159 -0
- package/dist/commands/docs.js +119 -0
- package/dist/commands/doctor.js +284 -0
- package/dist/commands/graph.js +34 -0
- package/dist/commands/handoff.js +108 -0
- package/dist/commands/init.js +280 -0
- package/dist/commands/lane.js +12 -0
- package/dist/commands/list.js +20 -0
- package/dist/commands/login.js +134 -0
- package/dist/commands/logout.js +44 -0
- package/dist/commands/open.js +54 -0
- package/dist/commands/organization.js +114 -0
- package/dist/commands/review.js +152 -0
- package/dist/commands/search.js +57 -0
- package/dist/commands/sessions.js +62 -0
- package/dist/commands/sync.js +85 -0
- package/dist/commands/telemetry.js +112 -0
- package/dist/commands/toolkit.js +167 -0
- package/dist/commands/validate.js +67 -0
- package/dist/commands/workspace-sync.js +645 -0
- package/dist/commands/workspace.js +479 -0
- package/dist/index.js +357 -0
- package/dist/lib/agent/claudeHooks.js +77 -0
- package/dist/lib/auth_grant.js +398 -0
- package/dist/lib/auth_recovery.js +49 -0
- package/dist/lib/browser_opener.js +24 -0
- package/dist/lib/device_identity.js +28 -0
- package/dist/lib/device_keychain.js +101 -0
- package/dist/lib/env_file.js +75 -0
- package/dist/lib/etna/manager.js +43 -0
- package/dist/lib/etna/profile.js +41 -0
- package/dist/lib/etna/reconcile.js +77 -0
- package/dist/lib/filebeat_installer.js +246 -0
- package/dist/lib/git-sync/core/commit_range.js +37 -0
- package/dist/lib/git-sync/core/engine.js +263 -0
- package/dist/lib/git-sync/core/git_runner.js +137 -0
- package/dist/lib/git-sync/core/operation_state.js +22 -0
- package/dist/lib/git-sync/core/path_normalizer.js +98 -0
- package/dist/lib/git-sync/core/reconciliation_lock.js +103 -0
- package/dist/lib/git-sync/core/repository.js +49 -0
- package/dist/lib/git-sync/core/watch_stream.js +86 -0
- package/dist/lib/git-sync/core/watcher.js +61 -0
- package/dist/lib/git-sync/inbound/applier.js +131 -0
- package/dist/lib/git-sync/inbound/apply_commit.js +225 -0
- package/dist/lib/git-sync/inbound/approval.js +117 -0
- package/dist/lib/git-sync/inbound/content_decision.js +148 -0
- package/dist/lib/git-sync/inbound/download.js +223 -0
- package/dist/lib/git-sync/inbound/event_sync.js +133 -0
- package/dist/lib/git-sync/inbound/index.js +10 -0
- package/dist/lib/git-sync/inbound/notification.js +39 -0
- package/dist/lib/git-sync/inbound/state_fingerprint.js +32 -0
- package/dist/lib/git-sync/inbound/technical_wait.js +39 -0
- package/dist/lib/git-sync/inbound/undo.js +198 -0
- package/dist/lib/git-sync/outbound/extraction.js +107 -0
- package/dist/lib/git-sync/outbound/history_decision.js +112 -0
- package/dist/lib/git-sync/outbound/index.js +26 -0
- package/dist/lib/git-sync/outbound/manifest_builder.js +58 -0
- package/dist/lib/git-sync/outbound/publisher.js +51 -0
- package/dist/lib/git-sync/outbound/queue.js +123 -0
- package/dist/lib/git-sync/outbound/state.js +12 -0
- package/dist/lib/git-sync/outbound/upload_client.js +141 -0
- package/dist/lib/git-sync/recovery/bundle.js +56 -0
- package/dist/lib/git-sync/recovery/crypto.js +114 -0
- package/dist/lib/git-sync/recovery/index.js +3 -0
- package/dist/lib/git-sync/recovery/store.js +55 -0
- package/dist/lib/git-sync/whitelist/index.js +4 -0
- package/dist/lib/git-sync/whitelist/matcher.js +62 -0
- package/dist/lib/git-sync/whitelist/store.js +99 -0
- package/dist/lib/git-sync/whitelist/types.js +46 -0
- package/dist/lib/git-sync/whitelist/validation.js +60 -0
- package/dist/lib/graph/answer.js +171 -0
- package/dist/lib/graph/freshness.js +29 -0
- package/dist/lib/graph/library.js +19 -0
- package/dist/lib/guards.js +106 -0
- package/dist/lib/interactive.js +12 -0
- package/dist/lib/json_output.js +8 -0
- package/dist/lib/lanes/claudeAdapter.js +135 -0
- package/dist/lib/lanes/frames.js +73 -0
- package/dist/lib/lanes/supervisor.js +247 -0
- package/dist/lib/lanes/worktree.js +33 -0
- package/dist/lib/local_store/accepted_uploads.js +49 -0
- package/dist/lib/local_store/bindings.js +163 -0
- package/dist/lib/local_store/commit_origins.js +25 -0
- package/dist/lib/local_store/database.js +71 -0
- package/dist/lib/local_store/device_state.js +72 -0
- package/dist/lib/local_store/filebeat_state.js +133 -0
- package/dist/lib/local_store/handoff_applications.js +166 -0
- package/dist/lib/local_store/history_decisions.js +48 -0
- package/dist/lib/local_store/index.js +14 -0
- package/dist/lib/local_store/migrations.js +240 -0
- package/dist/lib/local_store/paths.js +24 -0
- package/dist/lib/local_store/pending_uploads.js +76 -0
- package/dist/lib/local_store/recovery_bundles.js +66 -0
- package/dist/lib/local_store/sync_vocabulary.js +33 -0
- package/dist/lib/local_store/workspace_events_cache.js +54 -0
- package/dist/lib/loopback.js +78 -0
- package/dist/lib/manifest.js +83 -0
- package/dist/lib/nustack_client.js +547 -0
- package/dist/lib/nustack_store.js +17 -0
- package/dist/lib/onboarding/attach_existing_git.js +65 -0
- package/dist/lib/onboarding/binding.js +91 -0
- package/dist/lib/onboarding/folder_state.js +99 -0
- package/dist/lib/onboarding/git_init.js +10 -0
- package/dist/lib/onboarding/handoff_apply.js +68 -0
- package/dist/lib/onboarding/init_fresh_folder.js +100 -0
- package/dist/lib/onboarding/manifest_writer.js +161 -0
- package/dist/lib/onboarding/web_first_attach.js +133 -0
- package/dist/lib/package_manager.js +23 -0
- package/dist/lib/review/client.js +66 -0
- package/dist/lib/schema_validator.js +24 -0
- package/dist/lib/secure_file.js +10 -0
- package/dist/lib/session_telemetry.js +263 -0
- package/dist/lib/sessions/client.js +48 -0
- package/dist/lib/sessions/outsideSessions.js +54 -0
- package/dist/lib/sse_framing.js +48 -0
- package/dist/lib/telemetry/config_renderer.js +175 -0
- package/dist/lib/telemetry/health_report.js +173 -0
- package/dist/lib/telemetry/provider_paths.js +38 -0
- package/dist/lib/telemetry/redaction_policy.js +90 -0
- package/dist/lib/telemetry/reload.js +84 -0
- package/dist/lib/workspace_context.js +121 -0
- package/dist/spec_schema.json +422 -0
- package/dist/wire/auth.js +1 -0
- package/dist/wire/sync-paths.js +25 -0
- package/dist/wire/telemetry.js +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { ensureAccessToken } from "../lib/auth_grant.js";
|
|
2
|
+
import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
3
|
+
import { makeReviewClient, ReviewUnreachableError } from "../lib/review/client.js";
|
|
4
|
+
import { printJsonSuccess } from "../lib/json_output.js";
|
|
5
|
+
const EXIT_FOR = {
|
|
6
|
+
signed: 0,
|
|
7
|
+
refused: 1,
|
|
8
|
+
none: 1,
|
|
9
|
+
unreachable: 2,
|
|
10
|
+
};
|
|
11
|
+
export function decisionStateOf(items) {
|
|
12
|
+
const latest = items.find((item) => item.answer?.verdict === "signed" || item.answer?.verdict === "refused");
|
|
13
|
+
if (!latest)
|
|
14
|
+
return "none";
|
|
15
|
+
return latest.answer?.verdict === "signed" ? "signed" : "refused";
|
|
16
|
+
}
|
|
17
|
+
function checkLine(state, commit) {
|
|
18
|
+
switch (state) {
|
|
19
|
+
case "signed":
|
|
20
|
+
return `review check: ${commit} is signed off\n`;
|
|
21
|
+
case "refused":
|
|
22
|
+
return `review check: ${commit} was refused\n`;
|
|
23
|
+
case "none":
|
|
24
|
+
return `review check: ${commit} has no answered decision\n`;
|
|
25
|
+
case "unreachable":
|
|
26
|
+
return `review check: NuStack is unreachable or this device is signed out\n`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function runReviewCheck(options, globals) {
|
|
30
|
+
const json = options.json === true;
|
|
31
|
+
const commit = options.commit?.trim() ?? "";
|
|
32
|
+
if (!commit) {
|
|
33
|
+
process.stderr.write("review check: --commit is required\n");
|
|
34
|
+
process.exitCode = 2;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const report = (state) => {
|
|
38
|
+
if (json)
|
|
39
|
+
printJsonSuccess("review check", { commit, state, exitCode: EXIT_FOR[state] });
|
|
40
|
+
else
|
|
41
|
+
process.stdout.write(checkLine(state, commit));
|
|
42
|
+
process.exitCode = EXIT_FOR[state];
|
|
43
|
+
};
|
|
44
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
45
|
+
const deviceAuth = await ensureAccessToken(baseUrl).catch(() => null);
|
|
46
|
+
if (!deviceAuth)
|
|
47
|
+
return report("unreachable");
|
|
48
|
+
try {
|
|
49
|
+
const items = await makeReviewClient(baseUrl, deviceAuth.accessToken).readDecisions(commit, options.repository?.trim() || null);
|
|
50
|
+
report(decisionStateOf(items));
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof ReviewUnreachableError)
|
|
54
|
+
return report("unreachable");
|
|
55
|
+
process.stderr.write(`review check: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
56
|
+
report("unreachable");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function readSubject(raw) {
|
|
60
|
+
if (!raw)
|
|
61
|
+
return null;
|
|
62
|
+
let parsed;
|
|
63
|
+
try {
|
|
64
|
+
parsed = JSON.parse(raw);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
70
|
+
return null;
|
|
71
|
+
const row = parsed;
|
|
72
|
+
const toolName = typeof row.toolName === "string" ? row.toolName.trim() : "";
|
|
73
|
+
if (!toolName)
|
|
74
|
+
return null;
|
|
75
|
+
const displayName = typeof row.displayName === "string" && row.displayName.trim() ? row.displayName.trim() : toolName;
|
|
76
|
+
return {
|
|
77
|
+
type: "tool_permission",
|
|
78
|
+
toolName,
|
|
79
|
+
displayName,
|
|
80
|
+
toolInput: row.toolInput ?? null,
|
|
81
|
+
...(typeof row.description === "string" ? { description: row.description } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function mirrorReport(json, state, id, note) {
|
|
85
|
+
if (json) {
|
|
86
|
+
printJsonSuccess("review mirror", { state, id, ...(note ? { note } : {}) });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (note)
|
|
90
|
+
process.stderr.write(`review mirror: ${note}\n`);
|
|
91
|
+
else
|
|
92
|
+
process.stdout.write(`review mirror: ${state}${id ? ` ${id}` : ""}\n`);
|
|
93
|
+
}
|
|
94
|
+
export async function runReviewMirror(options, globals) {
|
|
95
|
+
const json = options.json === true;
|
|
96
|
+
const state = options.state?.trim();
|
|
97
|
+
const sessionId = options.session?.trim() ?? "";
|
|
98
|
+
if (state !== "open" && state !== "answered") {
|
|
99
|
+
return mirrorReport(json, "skipped", null, "--state must be open or answered");
|
|
100
|
+
}
|
|
101
|
+
if (!sessionId)
|
|
102
|
+
return mirrorReport(json, "skipped", null, "--session is required");
|
|
103
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
104
|
+
const deviceAuth = await ensureAccessToken(baseUrl).catch(() => null);
|
|
105
|
+
if (!deviceAuth)
|
|
106
|
+
return mirrorReport(json, "skipped", null, "signed out");
|
|
107
|
+
const client = makeReviewClient(baseUrl, deviceAuth.accessToken);
|
|
108
|
+
try {
|
|
109
|
+
if (state === "answered") {
|
|
110
|
+
const id = options.id?.trim();
|
|
111
|
+
const verdict = options.verdict?.trim();
|
|
112
|
+
if (!id)
|
|
113
|
+
return mirrorReport(json, "skipped", null, "--id is required to answer a mirrored prompt");
|
|
114
|
+
if (verdict !== "allow" && verdict !== "deny") {
|
|
115
|
+
return mirrorReport(json, "skipped", null, "--verdict must be allow or deny");
|
|
116
|
+
}
|
|
117
|
+
const answered = await client.answerReviewItem(id, verdict);
|
|
118
|
+
return mirrorReport(json, "answered", answered.id);
|
|
119
|
+
}
|
|
120
|
+
const workspaceId = options.workspace?.trim() ?? "";
|
|
121
|
+
if (!workspaceId)
|
|
122
|
+
return mirrorReport(json, "skipped", null, "--workspace is required to open a mirrored prompt");
|
|
123
|
+
const subject = readSubject(options.subject);
|
|
124
|
+
if (!subject)
|
|
125
|
+
return mirrorReport(json, "skipped", null, "--subject must be a tool-permission JSON object");
|
|
126
|
+
const created = await client.createReviewItem({ workspaceId, sessionId, subject });
|
|
127
|
+
return mirrorReport(json, "open", created.id);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
return mirrorReport(json, "skipped", null, error instanceof Error ? error.message : String(error));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export function registerReviewCommands(reviewCommand, getGlobalOptions) {
|
|
134
|
+
reviewCommand
|
|
135
|
+
.command("check")
|
|
136
|
+
.description("Exit 0 when a person's sign-off stands against a commit, 1 when it does not, 2 when NuStack cannot be reached")
|
|
137
|
+
.option("--commit <sha>", "the commit the sign-off is owed against")
|
|
138
|
+
.option("--repository <url>", "narrow to one repository when a commit sha is ambiguous")
|
|
139
|
+
.option("--json", "emit the versioned machine envelope")
|
|
140
|
+
.action(async (options) => runReviewCheck(options, getGlobalOptions()));
|
|
141
|
+
reviewCommand
|
|
142
|
+
.command("mirror")
|
|
143
|
+
.description("Reflect a lane's permission prompt, or its answer, into the Review pane")
|
|
144
|
+
.option("--session <laneId>", "the lane's session id")
|
|
145
|
+
.option("--workspace <id>", "the Workspace the lane's folder is bound to (required with --state open)")
|
|
146
|
+
.option("--state <state>", "open or answered")
|
|
147
|
+
.option("--subject <json>", "the tool-permission subject, as JSON (required with --state open)")
|
|
148
|
+
.option("--verdict <verdict>", "allow or deny (required with --state answered)")
|
|
149
|
+
.option("--id <rowId>", "the mirrored row to answer (required with --state answered)")
|
|
150
|
+
.option("--json", "emit the versioned machine envelope")
|
|
151
|
+
.action(async (options) => runReviewMirror(options, getGlobalOptions()));
|
|
152
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { resolveNuStackUrl, searchWorkspaces, searchCatalogue, } from "../lib/nustack_client.js";
|
|
2
|
+
export async function runSearch(keywords, options, globals) {
|
|
3
|
+
const query = keywords.join(" ").trim();
|
|
4
|
+
if (!query) {
|
|
5
|
+
console.error("Usage: nustack search <keywords> — give at least one keyword.");
|
|
6
|
+
process.exitCode = 1;
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (options.limit !== undefined && (!Number.isInteger(options.limit) || options.limit <= 0)) {
|
|
10
|
+
console.error("Usage: nustack search --limit <n> — n must be a positive integer.");
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
15
|
+
const results = await searchWorkspaces(baseUrl, query, { category: options.category, limit: options.limit });
|
|
16
|
+
if (options.json) {
|
|
17
|
+
console.log(JSON.stringify(results, null, 2));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (results.length === 0) {
|
|
21
|
+
console.log(`No workspaces matched "${query}". Looking for a platform capability? Try \`nustack search services ${query}\`.`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
console.log(`${results.length} result${results.length === 1 ? "" : "s"} for "${query}"`);
|
|
25
|
+
for (const workspace of results) {
|
|
26
|
+
const category = workspace.categories[0] ?? "—";
|
|
27
|
+
console.log(`${workspace.displayName.padEnd(28)} ${category.padEnd(16)} ${workspace.tagline ?? ""}`);
|
|
28
|
+
const link = workspace.links[0];
|
|
29
|
+
if (link)
|
|
30
|
+
console.log(` ${link.url}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function runSearchServices(keywords, options, globals) {
|
|
34
|
+
const query = keywords.join(" ").trim();
|
|
35
|
+
if (!query) {
|
|
36
|
+
console.error("Usage: nustack search services <keywords> — give at least one keyword.");
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
41
|
+
const results = await searchCatalogue(baseUrl, query, { type: options.type, limit: options.limit });
|
|
42
|
+
if (options.json) {
|
|
43
|
+
console.log(JSON.stringify(results, null, 2));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (results.length === 0) {
|
|
47
|
+
console.log(`No services matched "${query}". Browse the full catalogue with \`nustack list\`.`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
console.log(`${results.length} result${results.length === 1 ? "" : "s"} for "${query}"`);
|
|
51
|
+
for (const service of results) {
|
|
52
|
+
const classifier = service.type ?? service.kind ?? "?";
|
|
53
|
+
const status = service.status ?? "?";
|
|
54
|
+
console.log(`${service.name.padEnd(28)} ${classifier.padEnd(14)} ${status.padEnd(12)} ${service.description ?? ""}`);
|
|
55
|
+
console.log(` → nustack add ${service.name}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { ensureAccessToken } from "../lib/auth_grant.js";
|
|
3
|
+
import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
4
|
+
import { makeSessionsClient } from "../lib/sessions/client.js";
|
|
5
|
+
import { recordOutsideSessions, } from "../lib/sessions/outsideSessions.js";
|
|
6
|
+
import { printJsonSuccess } from "../lib/json_output.js";
|
|
7
|
+
async function readScanArgument(raw) {
|
|
8
|
+
const body = raw.startsWith("@") ? await readFile(raw.slice(1), "utf8") : raw;
|
|
9
|
+
return JSON.parse(body);
|
|
10
|
+
}
|
|
11
|
+
function readTranscripts(value) {
|
|
12
|
+
if (!Array.isArray(value))
|
|
13
|
+
return [];
|
|
14
|
+
const transcripts = [];
|
|
15
|
+
for (const entry of value) {
|
|
16
|
+
if (!entry || typeof entry !== "object")
|
|
17
|
+
continue;
|
|
18
|
+
const row = entry;
|
|
19
|
+
const sessionKey = typeof row.sessionKey === "string" ? row.sessionKey : null;
|
|
20
|
+
const provider = typeof row.provider === "string" ? row.provider : null;
|
|
21
|
+
const modifiedMs = typeof row.modifiedMs === "number" ? row.modifiedMs : null;
|
|
22
|
+
if (!sessionKey || !provider || modifiedMs === null)
|
|
23
|
+
continue;
|
|
24
|
+
transcripts.push({
|
|
25
|
+
sessionKey,
|
|
26
|
+
provider,
|
|
27
|
+
modifiedMs,
|
|
28
|
+
workspaceId: typeof row.workspaceId === "string" && row.workspaceId ? row.workspaceId : null,
|
|
29
|
+
vendorSessionId: typeof row.id === "string" && row.id ? row.id : null,
|
|
30
|
+
...(typeof row.sizeBytes === "number" ? { sizeBytes: row.sizeBytes } : {}),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return transcripts;
|
|
34
|
+
}
|
|
35
|
+
const EMPTY = { created: 0, finished: 0, skipped: 0, failed: 0 };
|
|
36
|
+
function report(command, summary, json, note) {
|
|
37
|
+
if (json) {
|
|
38
|
+
printJsonSuccess(command, { ...summary, ...(note ? { note } : {}) });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
process.stdout.write(`sessions record: ${summary.created} created, ${summary.finished} finished, ${summary.skipped} unchanged, ${summary.failed} failed${note ? ` (${note})` : ""}\n`);
|
|
42
|
+
}
|
|
43
|
+
export async function runSessionsRecord(options, globals) {
|
|
44
|
+
const json = options.json === true;
|
|
45
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
46
|
+
if (!options.scan)
|
|
47
|
+
return report("sessions record", EMPTY, json, "no scan payload");
|
|
48
|
+
const payload = (await readScanArgument(options.scan).catch(() => null));
|
|
49
|
+
const transcripts = readTranscripts(payload?.sessions);
|
|
50
|
+
if (transcripts.length === 0)
|
|
51
|
+
return report("sessions record", EMPTY, json, "nothing to record");
|
|
52
|
+
const deviceAuth = await ensureAccessToken(baseUrl).catch(() => null);
|
|
53
|
+
if (!deviceAuth)
|
|
54
|
+
return report("sessions record", EMPTY, json, "signed out");
|
|
55
|
+
const summary = await recordOutsideSessions(makeSessionsClient(baseUrl, deviceAuth.accessToken), {
|
|
56
|
+
transcripts,
|
|
57
|
+
laneSessionIds: Array.isArray(payload?.laneSessionIds)
|
|
58
|
+
? payload.laneSessionIds.filter((id) => typeof id === "string")
|
|
59
|
+
: [],
|
|
60
|
+
});
|
|
61
|
+
report("sessions record", summary, json);
|
|
62
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import { exchangeServiceCredential, fetchServiceCredential, listWorkspaceIntegrations, resolveNuStackUrl, } from "../lib/nustack_client.js";
|
|
4
|
+
import { readConsumesEdges } from "../lib/manifest.js";
|
|
5
|
+
import { ensureAccessToken, signedOutCopy } from "../lib/auth_grant.js";
|
|
6
|
+
import { resolveWorkspaceId } from "../lib/workspace_context.js";
|
|
7
|
+
import { readManagedValues, writeManagedValues } from "../lib/env_file.js";
|
|
8
|
+
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
9
|
+
const COMMAND = "sync";
|
|
10
|
+
const signedOut = () => signedOutCopy();
|
|
11
|
+
export async function runSync(options) {
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const baseUrl = resolveNuStackUrl(options);
|
|
14
|
+
const json = options.json === true;
|
|
15
|
+
if (!json)
|
|
16
|
+
p.intro("nustack sync");
|
|
17
|
+
const deviceAuth = await ensureAccessToken(baseUrl).catch(() => null);
|
|
18
|
+
if (!deviceAuth)
|
|
19
|
+
return failSync(json, "SIGNED_OUT", signedOut(), "The managed block was not touched.");
|
|
20
|
+
const accessToken = deviceAuth.accessToken;
|
|
21
|
+
const workspace = await resolveWorkspaceId(cwd, { workspace: options.workspace });
|
|
22
|
+
if (!workspace.ok)
|
|
23
|
+
return failSync(json, "WORKSPACE_UNRESOLVED", workspace.message, "The managed block was not touched.");
|
|
24
|
+
const spinner = json ? null : p.spinner();
|
|
25
|
+
spinner?.start("Exchanging declared services");
|
|
26
|
+
let activeServices;
|
|
27
|
+
let projections;
|
|
28
|
+
try {
|
|
29
|
+
for (const edge of await readConsumesEdges(cwd)) {
|
|
30
|
+
await exchangeServiceCredential(baseUrl, accessToken, { workspaceId: workspace.workspaceId, service: edge.service });
|
|
31
|
+
}
|
|
32
|
+
spinner?.message("Enumerating active integrations");
|
|
33
|
+
const integrations = await listWorkspaceIntegrations(baseUrl, accessToken, workspace.workspaceId);
|
|
34
|
+
activeServices = integrations.filter((i) => i.status === "active").map((i) => i.serviceSlug);
|
|
35
|
+
projections = [];
|
|
36
|
+
for (const service of activeServices) {
|
|
37
|
+
projections.push(await fetchServiceCredential(baseUrl, accessToken, { workspaceId: workspace.workspaceId, service }));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
spinner?.stop("Projection failed", 1);
|
|
42
|
+
return failSync(json, "PROJECTION_FAILED", error instanceof Error ? error.message : String(error), "The managed block was not touched.");
|
|
43
|
+
}
|
|
44
|
+
spinner?.stop(`${projections.length} service credential(s) projected`);
|
|
45
|
+
const managedValues = await readManagedValues(path.join(cwd, ".env"));
|
|
46
|
+
const nextValues = {};
|
|
47
|
+
if (managedValues.NUSTACK_WORKSPACE_ID)
|
|
48
|
+
nextValues.NUSTACK_WORKSPACE_ID = managedValues.NUSTACK_WORKSPACE_ID;
|
|
49
|
+
for (const credential of [...projections].sort((a, b) => a.service.localeCompare(b.service))) {
|
|
50
|
+
const prefix = toEnvPrefix(credential.service);
|
|
51
|
+
nextValues[`${prefix}_ENDPOINT`] = credential.endpoint;
|
|
52
|
+
nextValues[`${prefix}_KEY`] = credential.serviceKey;
|
|
53
|
+
}
|
|
54
|
+
const removedKeys = Object.keys(managedValues).filter((key) => !(key in nextValues));
|
|
55
|
+
const placeholders = {};
|
|
56
|
+
for (const key of Object.keys(nextValues))
|
|
57
|
+
placeholders[key] = "";
|
|
58
|
+
await writeManagedValues(path.join(cwd, ".env"), nextValues);
|
|
59
|
+
await writeManagedValues(path.join(cwd, ".env.example"), placeholders);
|
|
60
|
+
if (json) {
|
|
61
|
+
printJsonSuccess(COMMAND, {
|
|
62
|
+
workspaceId: workspace.workspaceId,
|
|
63
|
+
integrations: activeServices.map((service) => ({ service, status: "active" })),
|
|
64
|
+
envKeysWritten: Object.keys(nextValues),
|
|
65
|
+
removedKeys,
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
p.log.success(`Managed block rewritten: ${projections.length} service credential(s).`);
|
|
70
|
+
if (removedKeys.length > 0)
|
|
71
|
+
p.log.info(`Removed stale entries: ${removedKeys.join(", ")}`);
|
|
72
|
+
p.outro("In sync with NuStack — Service credentials only (Documents & Git are `nustack workspace sync`).");
|
|
73
|
+
}
|
|
74
|
+
function failSync(json, code, message, humanTail) {
|
|
75
|
+
if (json) {
|
|
76
|
+
printJsonError(COMMAND, { code, message });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
p.log.error(message);
|
|
80
|
+
p.outro(humanTail);
|
|
81
|
+
process.exitCode = 1;
|
|
82
|
+
}
|
|
83
|
+
function toEnvPrefix(serviceName) {
|
|
84
|
+
return serviceName.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
85
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
3
|
+
import { printJsonSuccess } from "../lib/json_output.js";
|
|
4
|
+
import { openLocalStore } from "../lib/local_store/database.js";
|
|
5
|
+
import { getBindingByRepositoryRoot, listActiveBindings, setBindingTelemetry, } from "../lib/local_store/bindings.js";
|
|
6
|
+
import { upsertFilebeatState } from "../lib/local_store/filebeat_state.js";
|
|
7
|
+
import { ensureFilebeatBinary, unregisterService } from "../lib/filebeat_installer.js";
|
|
8
|
+
import { collectTelemetryStatus } from "../lib/telemetry/health_report.js";
|
|
9
|
+
import { ensureBindingTelemetry } from "../lib/session_telemetry.js";
|
|
10
|
+
import { resolveRepository } from "../lib/git-sync/core/repository.js";
|
|
11
|
+
function resolveTargetBinding(store, options) {
|
|
12
|
+
if (options.device)
|
|
13
|
+
return null;
|
|
14
|
+
try {
|
|
15
|
+
const { repositoryRoot } = resolveRepository(options.cwd ?? process.cwd());
|
|
16
|
+
return getBindingByRepositoryRoot(store, repositoryRoot);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function folderLabel(binding) {
|
|
23
|
+
const segments = binding.repositoryRoot.split("/").filter(Boolean);
|
|
24
|
+
return segments[segments.length - 1] ?? binding.id;
|
|
25
|
+
}
|
|
26
|
+
export async function runTelemetry(action, options) {
|
|
27
|
+
const json = options.json ?? false;
|
|
28
|
+
const store = await openLocalStore();
|
|
29
|
+
try {
|
|
30
|
+
const verbose = { localVerbose: options.localVerbose === true };
|
|
31
|
+
if (action === "status") {
|
|
32
|
+
const status = await collectTelemetryStatus(store, undefined, verbose);
|
|
33
|
+
if (json) {
|
|
34
|
+
printJsonSuccess("telemetry status", status);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const shipping = status.folders.filter((f) => f.enabled).length;
|
|
38
|
+
p.intro("nustack telemetry status");
|
|
39
|
+
p.note([
|
|
40
|
+
`telemetry_enabled: ${status.telemetryEnabled}`,
|
|
41
|
+
`env opt-out: ${status.optedOutByEnv}`,
|
|
42
|
+
`binary: ${status.binaryPath ?? "not installed"}`,
|
|
43
|
+
`version: desired ${status.desiredVersion ?? "—"} / observed ${status.observedVersion ?? "—"}`,
|
|
44
|
+
`service: ${status.serviceRegistered ? "registered" : "not registered"}`,
|
|
45
|
+
`install state: ${status.filebeatState}${status.lastErrorCode ? ` (${status.lastErrorCode})` : ""}`,
|
|
46
|
+
`config version: ${status.desiredConfigVersion}`,
|
|
47
|
+
`folders: ${shipping} of ${status.folders.length} shipping`,
|
|
48
|
+
`last cloud report: ${status.lastHealthAtMs ? new Date(status.lastHealthAtMs).toISOString() : "never"}`,
|
|
49
|
+
].join("\n"), "Telemetry");
|
|
50
|
+
p.outro("Telemetry status.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const target = action === "reinstall" ? null : resolveTargetBinding(store, options);
|
|
54
|
+
if (target) {
|
|
55
|
+
const enabled = action === "enable";
|
|
56
|
+
setBindingTelemetry(store, target.id, enabled);
|
|
57
|
+
const baseUrl = resolveNuStackUrl(options);
|
|
58
|
+
await ensureBindingTelemetry({ baseUrl, store, binding: { ...target, telemetryEnabled: enabled } });
|
|
59
|
+
const status = await collectTelemetryStatus(store, undefined, verbose);
|
|
60
|
+
if (json) {
|
|
61
|
+
printJsonSuccess(`telemetry ${action}`, {
|
|
62
|
+
scope: "folder",
|
|
63
|
+
bindingId: target.id,
|
|
64
|
+
folderEnabled: enabled,
|
|
65
|
+
...status,
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
p.intro(`nustack telemetry ${action}`);
|
|
70
|
+
p.log.success(enabled
|
|
71
|
+
? `${folderLabel(target)} is shipping. Other folders are unchanged.`
|
|
72
|
+
: `${folderLabel(target)} has stopped shipping and its input config is removed. Other folders are unchanged.`);
|
|
73
|
+
p.outro(`Run \`nustack telemetry ${enabled ? "disable" : "enable"}\` here to reverse it.`);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (action === "disable") {
|
|
77
|
+
unregisterService();
|
|
78
|
+
upsertFilebeatState(store, { telemetryEnabled: false, processState: "stopped" });
|
|
79
|
+
if (json) {
|
|
80
|
+
printJsonSuccess("telemetry disable", {
|
|
81
|
+
scope: "device",
|
|
82
|
+
bindingId: null,
|
|
83
|
+
telemetryEnabled: false,
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
p.log.success("Stopped the telemetry shipper on this machine; every folder reports disabled (Document sync untouched).");
|
|
88
|
+
p.outro("Run `nustack telemetry enable` to resume.");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const baseUrl = resolveNuStackUrl(options);
|
|
92
|
+
upsertFilebeatState(store, { telemetryEnabled: true });
|
|
93
|
+
if (action === "reinstall")
|
|
94
|
+
await ensureFilebeatBinary(() => { });
|
|
95
|
+
const bindings = listActiveBindings(store);
|
|
96
|
+
for (const binding of bindings) {
|
|
97
|
+
await ensureBindingTelemetry({ baseUrl, store, binding });
|
|
98
|
+
}
|
|
99
|
+
const status = await collectTelemetryStatus(store, undefined, verbose);
|
|
100
|
+
if (json) {
|
|
101
|
+
printJsonSuccess(`telemetry ${action}`, { scope: "device", bindingId: null, ...status });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const shipping = bindings.filter((b) => b.telemetryEnabled).length;
|
|
105
|
+
p.intro(`nustack telemetry ${action}`);
|
|
106
|
+
p.log.success(`Reconciled ${shipping} of ${bindings.length} folder${bindings.length === 1 ? "" : "s"}; install state ${status.filebeatState}.`);
|
|
107
|
+
p.outro(action === "reinstall" ? "Reinstalled." : "Enabled.");
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
store.close();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
3
|
+
import { ensureAuthWithRecovery, lastAuthFailure } from "../lib/auth_recovery.js";
|
|
4
|
+
import { resolveWorkspaceId } from "../lib/workspace_context.js";
|
|
5
|
+
import { isInteractive } from "../lib/interactive.js";
|
|
6
|
+
import { openLocalStore } from "../lib/local_store/database.js";
|
|
7
|
+
import { getBindingByRepositoryRoot } from "../lib/local_store/bindings.js";
|
|
8
|
+
import { resolveRepository } from "../lib/git-sync/core/repository.js";
|
|
9
|
+
import { readWorkspaceManifests, diagnoseManifests } from "../lib/onboarding/manifest_writer.js";
|
|
10
|
+
import { fetchToolkitProfile, ToolkitProfileUnavailableError } from "../lib/etna/profile.js";
|
|
11
|
+
import { buildReconcilePlan, applyReconcile, toolkitCompatibility } from "../lib/etna/reconcile.js";
|
|
12
|
+
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
13
|
+
function emitFail(command, json, code, message) {
|
|
14
|
+
if (json)
|
|
15
|
+
printJsonError(command, { code, message });
|
|
16
|
+
else {
|
|
17
|
+
console.error(message);
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function resolveContext(command, json, options, globals) {
|
|
22
|
+
let root;
|
|
23
|
+
try {
|
|
24
|
+
root = resolveRepository(process.cwd()).repositoryRoot;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
emitFail(command, json, "NOT_A_REPOSITORY", "Run `nustack toolkit` inside a bound Git repository.");
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
31
|
+
const workspace = await resolveWorkspaceId(root, options);
|
|
32
|
+
if (!workspace.ok) {
|
|
33
|
+
emitFail(command, json, "WORKSPACE_CONTEXT_MISSING", workspace.message);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const deviceAuth = await ensureAuthWithRecovery(baseUrl, json);
|
|
37
|
+
if (!deviceAuth) {
|
|
38
|
+
const failure = lastAuthFailure();
|
|
39
|
+
emitFail(command, json, failure.code, failure.message);
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return { baseUrl, accessToken: deviceAuth.accessToken, workspaceId: workspace.workspaceId, root };
|
|
43
|
+
}
|
|
44
|
+
export async function runToolkitStatus(options, globals) {
|
|
45
|
+
const json = options.json === true;
|
|
46
|
+
const ctx = await resolveContext("toolkit status", json, options, globals);
|
|
47
|
+
if (!ctx)
|
|
48
|
+
return;
|
|
49
|
+
let cloud;
|
|
50
|
+
try {
|
|
51
|
+
cloud = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, ctx.workspaceId);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error instanceof ToolkitProfileUnavailableError) {
|
|
55
|
+
emitFail("toolkit status", json, "HARNESS_PROFILE_UNAVAILABLE", error.message);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
const manifests = await readWorkspaceManifests(ctx.root);
|
|
61
|
+
const diagnosis = diagnoseManifests({ name: cloud.name, version: cloud.version }, manifests.workspaceManifest?.toolkit ?? null, manifests.serviceToolkit);
|
|
62
|
+
const binding = await withStore((store) => getBindingByRepositoryRoot(store, ctx.root));
|
|
63
|
+
const compatibility = toolkitCompatibility(binding ?? { etnaProfileJson: null }, cloud);
|
|
64
|
+
if (json) {
|
|
65
|
+
printJsonSuccess("toolkit status", {
|
|
66
|
+
workspaceId: ctx.workspaceId,
|
|
67
|
+
cloud,
|
|
68
|
+
workspaceJson: manifests.workspaceManifest?.toolkit ?? null,
|
|
69
|
+
serviceJson: manifests.serviceToolkit,
|
|
70
|
+
diagnosis,
|
|
71
|
+
compatibility,
|
|
72
|
+
});
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
console.log(`Toolkit for Workspace ${ctx.workspaceId}`);
|
|
76
|
+
console.log(` cloud pin: ${cloud.name}@${cloud.version} (renderer ${cloud.rendererVersion}, placement ${cloud.placementMapVersion})`);
|
|
77
|
+
console.log(` .nustack/app.json: ${fmtToolkit(manifests.workspaceManifest?.toolkit)}`);
|
|
78
|
+
console.log(` nustack.service.json:${fmtToolkit(manifests.serviceToolkit)}`);
|
|
79
|
+
console.log(` diagnosis: ${diagnosis}`);
|
|
80
|
+
console.log(` write gate: ${compatibility.blocksWrites ? `BLOCKED — ${compatibility.reason}` : "clear (projection and sync writes allowed)"}`);
|
|
81
|
+
if (diagnosis !== "aligned" && diagnosis !== "missing") {
|
|
82
|
+
console.log(" Repair with `nustack toolkit reconcile`.");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export async function runToolkitReconcile(options, globals) {
|
|
86
|
+
const json = options.json === true;
|
|
87
|
+
const ctx = await resolveContext("toolkit reconcile", json, options, globals);
|
|
88
|
+
if (!ctx)
|
|
89
|
+
return;
|
|
90
|
+
let cloud;
|
|
91
|
+
try {
|
|
92
|
+
cloud = await fetchToolkitProfile(ctx.baseUrl, ctx.accessToken, ctx.workspaceId);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof ToolkitProfileUnavailableError) {
|
|
96
|
+
emitFail("toolkit reconcile", json, "HARNESS_PROFILE_UNAVAILABLE", error.message);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
const binding = await withStore((store) => getBindingByRepositoryRoot(store, ctx.root));
|
|
102
|
+
const manifests = await readWorkspaceManifests(ctx.root);
|
|
103
|
+
const identity = manifests.workspaceManifest
|
|
104
|
+
? { workspaceId: manifests.workspaceManifest.workspaceId, organizationId: manifests.workspaceManifest.organizationId }
|
|
105
|
+
: manifests.serviceWorkspaceBinding;
|
|
106
|
+
if (!identity) {
|
|
107
|
+
emitFail("toolkit reconcile", json, "WORKSPACE_CONTEXT_MISSING", "No `.nustack/app.json` binding to reconcile — attach the repository first.");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const plan = await buildReconcilePlan(ctx.root, binding ?? { etnaProfileJson: null }, cloud);
|
|
111
|
+
if (plan.noOp) {
|
|
112
|
+
if (json)
|
|
113
|
+
printJsonSuccess("toolkit reconcile", { applied: false, plan });
|
|
114
|
+
else
|
|
115
|
+
console.log("Toolkit already aligned with the cloud pin — nothing to reconcile.");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const confirmed = options.yes === true ? true : json ? false : await promptApply(plan);
|
|
119
|
+
if (!confirmed) {
|
|
120
|
+
if (json)
|
|
121
|
+
printJsonSuccess("toolkit reconcile", { applied: false, plan, note: "preview only — pass --yes to apply" });
|
|
122
|
+
else
|
|
123
|
+
console.log("Preview only — re-run with --yes to apply.");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await withStoreVoid(async (store) => {
|
|
127
|
+
await applyReconcile(store, binding?.id ?? "", ctx.root, identity, cloud);
|
|
128
|
+
});
|
|
129
|
+
if (json)
|
|
130
|
+
printJsonSuccess("toolkit reconcile", { applied: true, plan });
|
|
131
|
+
else
|
|
132
|
+
console.log(`Reconciled ${plan.changes.length} field(s) to the cloud pin ${cloud.name}@${cloud.version}.`);
|
|
133
|
+
}
|
|
134
|
+
async function promptApply(plan) {
|
|
135
|
+
if (!isInteractive())
|
|
136
|
+
return false;
|
|
137
|
+
p.intro("nustack toolkit reconcile");
|
|
138
|
+
p.note(plan.changes.map((c) => `${c.file} ${c.field}: ${c.from || "∅"} → ${c.to}`).join("\n"), "Repair preview");
|
|
139
|
+
const answer = await p.confirm({ message: "Apply this reconcile? No User content is touched." });
|
|
140
|
+
return !p.isCancel(answer) && answer === true;
|
|
141
|
+
}
|
|
142
|
+
function fmtToolkit(h) {
|
|
143
|
+
return h ? `${h.name}@${h.version}` : "(absent)";
|
|
144
|
+
}
|
|
145
|
+
async function withStore(fn) {
|
|
146
|
+
try {
|
|
147
|
+
const store = await openLocalStore();
|
|
148
|
+
try {
|
|
149
|
+
return fn(store);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
store.close();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function withStoreVoid(fn) {
|
|
160
|
+
const store = await openLocalStore();
|
|
161
|
+
try {
|
|
162
|
+
await fn(store);
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
store.close();
|
|
166
|
+
}
|
|
167
|
+
}
|