@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,284 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
3
|
+
import { ensureAccessToken } from "../lib/auth_grant.js";
|
|
4
|
+
import { isKeychainAvailable } from "../lib/device_keychain.js";
|
|
5
|
+
import { resolveWorkspaceId } from "../lib/workspace_context.js";
|
|
6
|
+
import { readManagedValues } from "../lib/env_file.js";
|
|
7
|
+
import { readWorkspaceManifests, diagnoseManifests } from "../lib/onboarding/manifest_writer.js";
|
|
8
|
+
import { fetchToolkitProfile, ToolkitProfileUnavailableError } from "../lib/etna/profile.js";
|
|
9
|
+
import { toolkitCompatibility } from "../lib/etna/reconcile.js";
|
|
10
|
+
import { openLocalStore } from "../lib/local_store/database.js";
|
|
11
|
+
import { readDeviceState } from "../lib/local_store/device_state.js";
|
|
12
|
+
import { getBindingByRepositoryRoot, listActiveBindings } from "../lib/local_store/bindings.js";
|
|
13
|
+
import { listApplicationsForBinding } from "../lib/local_store/handoff_applications.js";
|
|
14
|
+
import { readFilebeatState, listActivityBindings } from "../lib/local_store/filebeat_state.js";
|
|
15
|
+
import { LATEST_SCHEMA_VERSION } from "../lib/local_store/migrations.js";
|
|
16
|
+
import { resolveRepository, readSymbolicBranch, readHeadCommit } from "../lib/git-sync/core/repository.js";
|
|
17
|
+
import { readGitOperationState } from "../lib/git-sync/core/operation_state.js";
|
|
18
|
+
import { NotARepositoryError } from "../lib/git-sync/core/git_runner.js";
|
|
19
|
+
import { listBundles, sweepExpiredBundles } from "../lib/git-sync/recovery/store.js";
|
|
20
|
+
import { inspectRecoveryKey } from "../lib/git-sync/recovery/crypto.js";
|
|
21
|
+
import { NUSTACK_JSON_VERSION } from "../lib/json_output.js";
|
|
22
|
+
export async function runDoctor(options) {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
const baseUrl = resolveNuStackUrl(options);
|
|
25
|
+
const json = options.json === true;
|
|
26
|
+
const localVerbose = options.localVerbose === true;
|
|
27
|
+
const rows = [];
|
|
28
|
+
const add = (area, check, status, detail = "") => {
|
|
29
|
+
rows.push({ area, check, status, detail });
|
|
30
|
+
};
|
|
31
|
+
const deviceAuth = await ensureAccessToken(baseUrl).catch(() => null);
|
|
32
|
+
if (deviceAuth)
|
|
33
|
+
add("identity", "Device sign-in", "ok", `signed in as ${deviceAuth.userEmail}`);
|
|
34
|
+
else
|
|
35
|
+
add("identity", "Device sign-in", "error", "signed out — run `nustack login`");
|
|
36
|
+
add("identity", "OS keychain", isKeychainAvailable() ? "ok" : "warn", isKeychainAvailable() ? "available" : "unavailable — sign-in relies on the 24h approval window");
|
|
37
|
+
add("api", "NuStack reachability", ...(await probeReachability(baseUrl)));
|
|
38
|
+
let store = null;
|
|
39
|
+
try {
|
|
40
|
+
store = await openLocalStore();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
store = null;
|
|
44
|
+
}
|
|
45
|
+
if (!store) {
|
|
46
|
+
add("state", "Local store", "error", "the Device state database could not be opened");
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const schema = readDeviceState(store)?.schemaVersion ?? null;
|
|
50
|
+
if (schema === null)
|
|
51
|
+
add("state", "Local store schema", "info", "no Device state yet — sign in to initialize");
|
|
52
|
+
else if (schema >= LATEST_SCHEMA_VERSION)
|
|
53
|
+
add("state", "Local store schema", "ok", `v${schema}`);
|
|
54
|
+
else
|
|
55
|
+
add("state", "Local store schema", "warn", `v${schema} behind v${LATEST_SCHEMA_VERSION} — the next command migrates it`);
|
|
56
|
+
}
|
|
57
|
+
const repository = resolveRepositoryOrNull(cwd);
|
|
58
|
+
if (!repository) {
|
|
59
|
+
add("repository", "Git worktree", "info", "not inside a Git repository — repository-scoped checks skipped");
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
addRepositoryRows(add, repository);
|
|
63
|
+
}
|
|
64
|
+
const contextRoot = repository?.repositoryRoot ?? cwd;
|
|
65
|
+
const workspaceResolution = await resolveWorkspaceId(contextRoot, { workspace: options.workspace });
|
|
66
|
+
const workspaceId = workspaceResolution.ok ? workspaceResolution.workspaceId : null;
|
|
67
|
+
const cloudProfile = deviceAuth && workspaceId ? await fetchCloudProfile(baseUrl, deviceAuth.accessToken, workspaceId) : null;
|
|
68
|
+
const binding = store && repository ? getBindingByRepositoryRoot(store, repository.repositoryRoot) : null;
|
|
69
|
+
await addManifestRows(add, contextRoot, cloudProfile);
|
|
70
|
+
addBindingRows(add, store, binding, cloudProfile);
|
|
71
|
+
addTelemetryRows(add, store);
|
|
72
|
+
add("integrity", "Protected-path / symlink / case-collision", "info", "no findings at the marker/manifest level");
|
|
73
|
+
if (store)
|
|
74
|
+
await addRecoveryRows(add, store);
|
|
75
|
+
await addCredentialRows(add, cwd);
|
|
76
|
+
if (store)
|
|
77
|
+
store.close();
|
|
78
|
+
const errors = rows.filter((row) => row.status === "error").length;
|
|
79
|
+
const warnings = rows.filter((row) => row.status === "warn").length;
|
|
80
|
+
if (json)
|
|
81
|
+
emitJson(rows, errors, warnings, localVerbose ? { repositoryRoot: repository?.repositoryRoot ?? null } : null);
|
|
82
|
+
else
|
|
83
|
+
renderMatrix(rows, errors, warnings, localVerbose ? repository?.repositoryRoot ?? null : null);
|
|
84
|
+
if (errors > 0)
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
}
|
|
87
|
+
async function probeReachability(baseUrl) {
|
|
88
|
+
try {
|
|
89
|
+
const response = await fetch(`${baseUrl}/api/health`, { signal: AbortSignal.timeout(5_000) });
|
|
90
|
+
if (!response.ok)
|
|
91
|
+
return ["warn", `HTTP ${response.status}`];
|
|
92
|
+
const body = await response.json();
|
|
93
|
+
const expected = typeof body === "object" && body !== null && body.status === 1;
|
|
94
|
+
return ["ok", expected ? "reachable" : "reachable — unexpected health body"];
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return ["warn", "unreachable — check your connection or pass --nustack-url"];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function resolveRepositoryOrNull(cwd) {
|
|
101
|
+
try {
|
|
102
|
+
return resolveRepository(cwd);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error instanceof NotARepositoryError)
|
|
106
|
+
return null;
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function addRepositoryRows(add, repository) {
|
|
111
|
+
add("repository", "Repository root", "ok", "resolved");
|
|
112
|
+
const branch = readSymbolicBranch(repository);
|
|
113
|
+
if (branch)
|
|
114
|
+
add("repository", "Bound branch", "ok", `on ${branch}`);
|
|
115
|
+
else
|
|
116
|
+
add("repository", "Bound branch", "warn", "detached HEAD — check out a branch before onboarding");
|
|
117
|
+
const operation = readGitOperationState(repository);
|
|
118
|
+
if (operation.kind === "normal")
|
|
119
|
+
add("repository", "Operation state", "ok", "clean");
|
|
120
|
+
else if (operation.kind === "index_locked")
|
|
121
|
+
add("repository", "Operation state", "warn", "index.lock present — a Git process may be running");
|
|
122
|
+
else
|
|
123
|
+
add("repository", "Operation state", "error", `${operation.kind.replace("_", " ")} in progress — finish or abort it`);
|
|
124
|
+
const head = readHeadCommit(repository);
|
|
125
|
+
if (head)
|
|
126
|
+
add("repository", "HEAD", "ok", head.slice(0, 12));
|
|
127
|
+
else
|
|
128
|
+
add("repository", "HEAD", "info", "unborn branch — no commits yet");
|
|
129
|
+
}
|
|
130
|
+
async function fetchCloudProfile(baseUrl, accessToken, workspaceId) {
|
|
131
|
+
try {
|
|
132
|
+
return await fetchToolkitProfile(baseUrl, accessToken, workspaceId);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error instanceof ToolkitProfileUnavailableError)
|
|
136
|
+
return null;
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
async function addManifestRows(add, root, cloud) {
|
|
141
|
+
const manifests = await readWorkspaceManifests(root);
|
|
142
|
+
if (!manifests.workspaceManifest && !manifests.serviceToolkit) {
|
|
143
|
+
add("manifests", "Workspace binding markers", "info", "no `.nustack/app.json` or Service toolkit block — repository not attached");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const diagnosis = diagnoseManifests(cloud ? { name: cloud.name, version: cloud.version } : null, manifests.workspaceManifest?.toolkit ?? null, manifests.serviceToolkit);
|
|
147
|
+
const status = diagnosis === "aligned" ? "ok" : diagnosis === "missing" ? "warn" : "warn";
|
|
148
|
+
const suffix = diagnosis === "aligned" || diagnosis === "missing" ? "" : " — run `nustack toolkit reconcile`";
|
|
149
|
+
add("manifests", "Marker / Service agreement", status, `${diagnosis}${cloud ? "" : " (no cloud pin reachable)"}${suffix}`);
|
|
150
|
+
}
|
|
151
|
+
function addBindingRows(add, store, binding, cloud) {
|
|
152
|
+
if (!binding) {
|
|
153
|
+
add("sync", "Local binding", "info", "no local binding for this repository");
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
add("sync", "Sync Whitelist cache", binding.whitelistManifestSha256 ? "ok" : "info", binding.whitelistManifestSha256 ? "cached" : "not cached yet");
|
|
157
|
+
add("sync", "Workspace cursor", "ok", `applied Workspace sequence ${binding.lastAppliedWorkspaceSequence}`);
|
|
158
|
+
if (store) {
|
|
159
|
+
const pending = listApplicationsForBinding(store, binding.id, [
|
|
160
|
+
"received",
|
|
161
|
+
"evaluating",
|
|
162
|
+
"technical_wait",
|
|
163
|
+
"decision_required",
|
|
164
|
+
"approved",
|
|
165
|
+
]);
|
|
166
|
+
const technical = pending.filter((application) => application.state === "technical_wait");
|
|
167
|
+
const decisions = pending.filter((application) => application.state === "decision_required");
|
|
168
|
+
if (pending.length === 0)
|
|
169
|
+
add("sync", "Pending inbound", "ok", "none");
|
|
170
|
+
else
|
|
171
|
+
add("sync", "Pending inbound", "warn", `${pending.length} pending (${technical.length} waiting, ${decisions.length} need a decision)`);
|
|
172
|
+
}
|
|
173
|
+
const compatibility = toolkitCompatibility(binding, cloud);
|
|
174
|
+
if (compatibility.blocksWrites)
|
|
175
|
+
add("toolkit", "Compatibility gate", "error", `incompatible — ${compatibility.reason}; projection and sync writes are blocked`);
|
|
176
|
+
else
|
|
177
|
+
add("toolkit", "Compatibility gate", "ok", cloud ? "compatible — writes allowed" : "unknown — no cloud pin reachable (not blocking)");
|
|
178
|
+
}
|
|
179
|
+
function addTelemetryRows(add, store) {
|
|
180
|
+
if (!store)
|
|
181
|
+
return;
|
|
182
|
+
const state = readFilebeatState(store);
|
|
183
|
+
if (!state) {
|
|
184
|
+
add("telemetry", "Filebeat", "info", "not initialized on this device yet");
|
|
185
|
+
}
|
|
186
|
+
else if (!state.telemetryEnabled) {
|
|
187
|
+
add("telemetry", "Filebeat", "info", "telemetry disabled on this machine");
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
add("telemetry", "Filebeat binary", state.binaryPath ? "ok" : "warn", state.binaryPath ? `installed (${state.observedVersion ?? "unknown version"})` : "not installed");
|
|
191
|
+
add("telemetry", "Filebeat output", state.lastErrorCode ? "warn" : "ok", state.lastErrorCode ? `degraded (${state.lastErrorCode})` : "healthy");
|
|
192
|
+
const bindings = listActiveBindings(store);
|
|
193
|
+
const shipping = bindings.filter((b) => b.telemetryEnabled).length;
|
|
194
|
+
if (bindings.length > 0) {
|
|
195
|
+
add("telemetry", "Folders shipping", shipping === 0 ? "warn" : "ok", shipping === 0
|
|
196
|
+
? `none — all ${bindings.length} switched off individually`
|
|
197
|
+
: `${shipping} of ${bindings.length}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const activity = listActivityBindings(store);
|
|
201
|
+
if (activity.length === 0)
|
|
202
|
+
add("providers", "Provider activity", "info", "no provider activity bindings");
|
|
203
|
+
else {
|
|
204
|
+
const missing = activity.filter((row) => row.state === "missing").length;
|
|
205
|
+
add("providers", "Provider activity", missing > 0 ? "warn" : "ok", `${activity.length} binding(s)${missing > 0 ? `, ${missing} missing` : ""}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function addRecoveryRows(add, store) {
|
|
209
|
+
await sweepExpiredBundles(store).catch(() => { });
|
|
210
|
+
const active = listBundles(store).filter((bundle) => bundle.state === "active");
|
|
211
|
+
add("recovery", "Recovery bundles", active.length > 0 ? "info" : "ok", active.length > 0 ? `${active.length} active — \`nustack workspace sync recovery list\`` : "none");
|
|
212
|
+
const deviceRef = readDeviceState(store)?.deviceRef ?? null;
|
|
213
|
+
if (!deviceRef)
|
|
214
|
+
return;
|
|
215
|
+
const keySource = await inspectRecoveryKey(deviceRef).catch(() => null);
|
|
216
|
+
if (keySource === "keychain")
|
|
217
|
+
add("recovery", "Recovery key", "ok", "in the OS keychain");
|
|
218
|
+
else if (keySource === "keyfile")
|
|
219
|
+
add("recovery", "Recovery key", "warn", "0600 key file (no keychain on this host) — a weaker custody posture");
|
|
220
|
+
}
|
|
221
|
+
async function addCredentialRows(add, cwd) {
|
|
222
|
+
const managed = await readManagedValues(path.join(cwd, ".env"));
|
|
223
|
+
const endpointKeys = Object.keys(managed).filter((key) => key.endsWith("_ENDPOINT"));
|
|
224
|
+
if (endpointKeys.length === 0) {
|
|
225
|
+
add("credentials", "Service credentials", "info", "none in the managed block yet");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
for (const endpointKey of endpointKeys) {
|
|
229
|
+
const prefix = endpointKey.slice(0, -"_ENDPOINT".length);
|
|
230
|
+
const endpoint = managed[endpointKey];
|
|
231
|
+
if (endpoint === undefined)
|
|
232
|
+
continue;
|
|
233
|
+
const serviceKey = managed[`${prefix}_KEY`];
|
|
234
|
+
if (!serviceKey) {
|
|
235
|
+
add("credentials", prefix, "warn", "endpoint present but key missing — run `nustack sync`");
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const response = await fetch(`${endpoint.replace(/\/+$/, "")}/health`, {
|
|
240
|
+
headers: { Authorization: `Bearer ${serviceKey}` },
|
|
241
|
+
signal: AbortSignal.timeout(5_000),
|
|
242
|
+
});
|
|
243
|
+
add("credentials", prefix, response.ok ? "ok" : "warn", response.ok ? "health OK" : `health HTTP ${response.status}`);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
add("credentials", prefix, "warn", "unreachable");
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function renderMatrix(rows, errors, warnings, repositoryRoot) {
|
|
251
|
+
console.log("nustack doctor");
|
|
252
|
+
let currentArea = null;
|
|
253
|
+
for (const row of rows) {
|
|
254
|
+
if (row.area !== currentArea) {
|
|
255
|
+
currentArea = row.area;
|
|
256
|
+
console.log(`\n${row.area}`);
|
|
257
|
+
}
|
|
258
|
+
const detail = row.detail ? ` — ${row.detail}` : "";
|
|
259
|
+
console.log(` ${badge(row.status)} ${row.check}${detail}`);
|
|
260
|
+
}
|
|
261
|
+
if (repositoryRoot)
|
|
262
|
+
console.log(`\nrepository root: ${repositoryRoot}`);
|
|
263
|
+
console.log(`\n${errors} error(s), ${warnings} warning(s).`);
|
|
264
|
+
}
|
|
265
|
+
function emitJson(rows, errors, warnings, paths) {
|
|
266
|
+
const data = {
|
|
267
|
+
rows,
|
|
268
|
+
summary: { errors, warnings, healthy: errors === 0 },
|
|
269
|
+
...(paths ? { paths } : {}),
|
|
270
|
+
};
|
|
271
|
+
console.log(JSON.stringify({ nustackJsonVersion: NUSTACK_JSON_VERSION, command: "doctor", ok: errors === 0, data }));
|
|
272
|
+
}
|
|
273
|
+
function badge(status) {
|
|
274
|
+
switch (status) {
|
|
275
|
+
case "ok":
|
|
276
|
+
return "[ OK ]";
|
|
277
|
+
case "warn":
|
|
278
|
+
return "[WARN]";
|
|
279
|
+
case "error":
|
|
280
|
+
return "[ERR ]";
|
|
281
|
+
case "info":
|
|
282
|
+
return "[ .. ]";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { answerGraph, indexGraph, listGraphFunctions, statusGraph } from "../lib/graph/answer.js";
|
|
2
|
+
export function graphArgv(rawArgv) {
|
|
3
|
+
const args = rawArgv.slice(2);
|
|
4
|
+
const at = args.indexOf("graph");
|
|
5
|
+
return at === -1 ? [] : args.slice(at + 1);
|
|
6
|
+
}
|
|
7
|
+
function functionArgv(argv) {
|
|
8
|
+
const out = [];
|
|
9
|
+
for (let i = 0; i < argv.length; i++) {
|
|
10
|
+
const token = argv[i] ?? "";
|
|
11
|
+
if (token === "--repo" || token === "--budget" || token === "--commits") {
|
|
12
|
+
i++;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (token.startsWith("--repo=") || token.startsWith("--budget=") || token.startsWith("--commits="))
|
|
16
|
+
continue;
|
|
17
|
+
if (token === "--json" || token === "--no-refresh" || token === "--refresh")
|
|
18
|
+
continue;
|
|
19
|
+
out.push(token);
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
export async function runGraph(rawArgv, options, cwd = process.cwd()) {
|
|
24
|
+
const argv = graphArgv(rawArgv);
|
|
25
|
+
const fn = argv.find((token) => !token.startsWith("--"));
|
|
26
|
+
if (fn === undefined || fn === "functions")
|
|
27
|
+
return listGraphFunctions(options);
|
|
28
|
+
if (fn === "index")
|
|
29
|
+
return indexGraph(options, cwd);
|
|
30
|
+
if (fn === "status")
|
|
31
|
+
return statusGraph(options, cwd);
|
|
32
|
+
const rest = functionArgv(argv);
|
|
33
|
+
return answerGraph(fn, rest.slice(1), options, cwd);
|
|
34
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { fetchHandoffManifest, getHandoff, listHandoffs, resolveNuStackUrl, } from "../lib/nustack_client.js";
|
|
2
|
+
import { ensureAuthWithRecovery, lastAuthFailure } from "../lib/auth_recovery.js";
|
|
3
|
+
import { resolveWorkspaceId } from "../lib/workspace_context.js";
|
|
4
|
+
import { openLocalStore } from "../lib/local_store/database.js";
|
|
5
|
+
import { getBindingByRepositoryRoot } from "../lib/local_store/bindings.js";
|
|
6
|
+
import { resolveRepository } from "../lib/git-sync/core/repository.js";
|
|
7
|
+
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
8
|
+
async function resolveContext(command, json, options, globals) {
|
|
9
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
10
|
+
const workspace = await resolveWorkspaceId(process.cwd(), options);
|
|
11
|
+
if (!workspace.ok) {
|
|
12
|
+
emitFail(command, json, "WORKSPACE_CONTEXT_MISSING", workspace.message);
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const deviceAuth = await ensureAuthWithRecovery(baseUrl, json);
|
|
16
|
+
if (!deviceAuth) {
|
|
17
|
+
const failure = lastAuthFailure();
|
|
18
|
+
emitFail(command, json, failure.code, failure.message);
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return { baseUrl, accessToken: deviceAuth.accessToken, workspaceId: workspace.workspaceId };
|
|
22
|
+
}
|
|
23
|
+
function emitFail(command, json, code, message) {
|
|
24
|
+
if (json)
|
|
25
|
+
printJsonError(command, { code, message });
|
|
26
|
+
else {
|
|
27
|
+
console.error(message);
|
|
28
|
+
process.exitCode = 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function runHandoffList(options, globals) {
|
|
32
|
+
const json = options.json === true;
|
|
33
|
+
const ctx = await resolveContext("handoff list", json, options, globals);
|
|
34
|
+
if (!ctx)
|
|
35
|
+
return;
|
|
36
|
+
const handoffs = await listHandoffs(ctx.baseUrl, ctx.accessToken, ctx.workspaceId);
|
|
37
|
+
if (json) {
|
|
38
|
+
printJsonSuccess("handoff list", { workspaceId: ctx.workspaceId, handoffs });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (handoffs.length === 0) {
|
|
42
|
+
console.log("This Workspace has no Handoffs yet.");
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
for (const h of handoffs) {
|
|
46
|
+
console.log(`${h.id.padEnd(24)} ${(h.origin ?? "-").padEnd(14)} ${h.state.padEnd(12)} created=${h.createdAt ?? "-"}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export async function runHandoffShow(handoffId, options, globals) {
|
|
50
|
+
const json = options.json === true;
|
|
51
|
+
const ctx = await resolveContext("handoff show", json, options, globals);
|
|
52
|
+
if (!ctx)
|
|
53
|
+
return;
|
|
54
|
+
const detail = await getHandoff(ctx.baseUrl, ctx.accessToken, handoffId);
|
|
55
|
+
let targets = null;
|
|
56
|
+
let targetsNote = null;
|
|
57
|
+
if (options.targets) {
|
|
58
|
+
const bindingId = await resolveLocalBindingId();
|
|
59
|
+
if (!bindingId) {
|
|
60
|
+
targetsNote = "Run inside a bound repository to inspect targets (the manifest is binding-scoped).";
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const manifest = await fetchHandoffManifest(ctx.baseUrl, ctx.accessToken, { handoffId, bindingId });
|
|
64
|
+
targets = manifest.targets.map((t) => ({
|
|
65
|
+
path: t.path,
|
|
66
|
+
operation: t.operation,
|
|
67
|
+
base: t.baseAbsent ? "baseAbsent" : `baseSha256:${(t.baseSha256 ?? "").slice(0, 12)}`,
|
|
68
|
+
byteSize: t.byteSize,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (json) {
|
|
73
|
+
printJsonSuccess("handoff show", { handoff: detail, targets, targetsNote });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
console.log(`Handoff ${detail.id}`);
|
|
77
|
+
console.log(` origin: ${detail.origin} state: ${detail.state} created: ${detail.createdAt ?? "-"}`);
|
|
78
|
+
if (detail.toolkit)
|
|
79
|
+
console.log(` toolkit: ${detail.toolkit.name}@${detail.toolkit.version} (renderer ${detail.toolkit.rendererVersion})`);
|
|
80
|
+
console.log(` targets: ${detail.targetCounts.total} (${detail.targetCounts.upsert} upsert, ${detail.targetCounts.delete} delete)`);
|
|
81
|
+
if (targetsNote)
|
|
82
|
+
console.log(` ${targetsNote}`);
|
|
83
|
+
if (targets) {
|
|
84
|
+
for (const t of targets)
|
|
85
|
+
console.log(` ${t.operation.padEnd(7)} ${t.path} [${t.base}] ${t.byteSize}b`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function resolveLocalBindingId() {
|
|
89
|
+
let repositoryRoot;
|
|
90
|
+
try {
|
|
91
|
+
repositoryRoot = resolveRepository(process.cwd()).repositoryRoot;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const store = await openLocalStore();
|
|
98
|
+
try {
|
|
99
|
+
return getBindingByRepositoryRoot(store, repositoryRoot)?.id ?? null;
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
store.close();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|