@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
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# `@nurix/nustack` — the `nustack` CLI
|
|
2
|
+
|
|
3
|
+
The developer's terminal surface for NuStack (`apps.nustack.ai`). It signs a machine in once through browser-approved device trust, then works against the same organizations, Workspaces, and documents the web studio serves — list and create Workspaces, read their documents, publish to the marketplace, search the public catalog, and mirror the machine's telemetry state.
|
|
4
|
+
|
|
5
|
+
Interactive UX is built on [`@clack/prompts`](https://github.com/bombshell-dev/clack).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Published to npm with **public access** — no npm account or token required to run it.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npx @nurix/nustack@latest --help # one-shot, no auth
|
|
13
|
+
npm install -g @nurix/nustack # or install the `nustack` binary globally
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The tarball ships no secrets. Authentication is the **device-trust flow**: `nustack login` registers this machine, you approve its code once in the browser (at `/device`, behind the studio's Google SSO), and the durable refresh material lands in your OS keychain — never in a file. Every authenticated command then rides a short-lived access token minted from it. Signing in on the CLI signs in the desktop workspace too: both clients share one keychain item and one `~/.nustack/device_id`.
|
|
17
|
+
|
|
18
|
+
A dead credential is never a dead end: if the server refuses this device's sign-in (revoked, stale), the CLI clears the dead keychain item and drops you straight back into `nustack login` — you never edit a config file by hand.
|
|
19
|
+
|
|
20
|
+
## Commands
|
|
21
|
+
|
|
22
|
+
### Identity & devices
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
nustack login [--no-open] # browser-approved device trust; re-login inside the
|
|
26
|
+
# 24h window needs no browser
|
|
27
|
+
nustack logout # clears the shared keychain item (signs out CLI + desktop)
|
|
28
|
+
nustack logout --revoke # server-side self-revoke first, then the local clear
|
|
29
|
+
nustack logout --forget-device # also mints a fresh machine identity on next login
|
|
30
|
+
nustack device list|status|revoke # this account's devices; revoke kills a machine's access
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Organizations & Workspaces
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
nustack organization list|create|use # memberships; `use` persists the active Organization,
|
|
37
|
+
# sent as the X-NuStack-Organization selection header
|
|
38
|
+
nustack workspace list # the active Organization's Workspaces
|
|
39
|
+
nustack workspace create [--prompt <text> | --name <name>]
|
|
40
|
+
# prompt-first: the first prompt creates the Workspace and
|
|
41
|
+
# streams the founding turn (SSE)
|
|
42
|
+
nustack workspace attach [--workspace <id>] # bind the current folder to a Workspace
|
|
43
|
+
nustack workspace status|open # cloud + local binding view; open in the browser
|
|
44
|
+
nustack workspace publish|unpublish # role-gated marketplace visibility
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Documents
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
nustack docs list [--workspace <id>] # the Workspace's documents: slug, type, status, written/empty
|
|
51
|
+
nustack docs show <slug-or-id> # the document body inline (bounded)
|
|
52
|
+
nustack docs status # per-document workability and row versions
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Discovery & health
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
nustack list # one line per catalogue service
|
|
59
|
+
nustack describe <svc> [--json] # full catalogue entry
|
|
60
|
+
nustack search <keywords...> # public Workspace search (the headline)
|
|
61
|
+
nustack search services <keywords...>
|
|
62
|
+
# service registry search (then `nustack add <name>`)
|
|
63
|
+
nustack graph [--mermaid] # consumes-graph from the catalogue
|
|
64
|
+
nustack validate [manifest] # nustack.service.json schema validation
|
|
65
|
+
nustack doctor # connectivity + manifest conformance probes
|
|
66
|
+
nustack open <svc> # open a service's endpoint or repo
|
|
67
|
+
nustack telemetry [status|enable|disable|reinstall]
|
|
68
|
+
# device-wide Filebeat telemetry control, mirrored server-side
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`nustack workspace sync …` (the document-sync engine) is present but dormant: every call reaches the studio Worker's `/api/*` catch-all `404`, and its disposition — grow a surface, keep it against a rebuilt binding seam, or drop it — is an open product decision tracked in the backlog.
|
|
72
|
+
|
|
73
|
+
## Conventions
|
|
74
|
+
|
|
75
|
+
- **`--json` almost everywhere** — most commands emit the `nustackJsonVersion: 1` machine envelope with `--json`; errors are typed (`{ error: { code, message } }`), never a bare HTTP status. Four surfaces break the pattern by design — `describe`, `search`, `search services`, and the `workspace sync` family emit a bare array/object instead, because the desktop and the agents pin those exact shapes.
|
|
76
|
+
- **Base URL** — `--nustack-url` flag → `NUSTACK_URL` env → the hosted default (`https://apps.nustack.ai`).
|
|
77
|
+
- **Organization context** — `nustack organization use` persists the selection locally; commands send it as a header the server re-proves against your memberships on every request.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import { fetchCatalogue, resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
4
|
+
import { detectPackageManager, installPackage } from "../lib/package_manager.js";
|
|
5
|
+
import { appendConsumesEdge, hasManifest, scaffoldManifest } from "../lib/manifest.js";
|
|
6
|
+
import { confirmOrFallback, isInteractive } from "../lib/interactive.js";
|
|
7
|
+
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
8
|
+
const COMMAND = "add";
|
|
9
|
+
export async function runAdd(serviceArg, options) {
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
const baseUrl = resolveNuStackUrl(options);
|
|
12
|
+
const json = options.json === true;
|
|
13
|
+
if (!json)
|
|
14
|
+
p.intro(`nustack add${serviceArg ? ` ${serviceArg}` : ""}`);
|
|
15
|
+
if (json && !serviceArg) {
|
|
16
|
+
printJsonError(COMMAND, { code: "SERVICE_REQUIRED", message: "nustack add --json requires an explicit service argument." });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const catalogue = await fetchCatalogue(baseUrl).catch(() => null);
|
|
20
|
+
const services = catalogue?.services ?? [];
|
|
21
|
+
const serviceName = serviceArg ?? (await pickService(services));
|
|
22
|
+
if (!serviceName) {
|
|
23
|
+
if (!json)
|
|
24
|
+
p.outro("Nothing was written.");
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const catalogueEntry = services.find((service) => service.name === serviceName);
|
|
29
|
+
if (!catalogueEntry) {
|
|
30
|
+
return failAdd(json, "SERVICE_NOT_IN_CATALOGUE", `'${serviceName}' is not in the catalogue at ${baseUrl}.`, "Nothing was written. Run `nustack list` to see what is available.");
|
|
31
|
+
}
|
|
32
|
+
if (!json) {
|
|
33
|
+
const endpoint = catalogueEntry.http?.endpoints?.dev ?? catalogueEntry.http?.endpoints?.production;
|
|
34
|
+
p.log.info(`${catalogueEntry.name}${catalogueEntry.type ? ` · ${catalogueEntry.type}` : ""}` +
|
|
35
|
+
`${catalogueEntry.description ? `\n${catalogueEntry.description}` : ""}` +
|
|
36
|
+
`${endpoint ? `\nEndpoint: ${endpoint}` : ""}`);
|
|
37
|
+
}
|
|
38
|
+
const packageInstalled = await maybeInstallPackage(catalogueEntry, cwd, json, options.yes === true);
|
|
39
|
+
const consumesRecorded = await recordConsumesEdge(catalogueEntry, serviceName, cwd, json, options.yes === true);
|
|
40
|
+
if (json) {
|
|
41
|
+
printJsonSuccess(COMMAND, {
|
|
42
|
+
service: serviceName,
|
|
43
|
+
packageInstalled,
|
|
44
|
+
consumesRecorded,
|
|
45
|
+
credential: "deferred",
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
p.log.info("No credential was issued — the credential plane is deferred by design, so `.env` was not touched.");
|
|
50
|
+
p.outro(`'${serviceName}' is recorded in this repo's manifest.`);
|
|
51
|
+
}
|
|
52
|
+
function failAdd(json, code, message, humanTail) {
|
|
53
|
+
if (json) {
|
|
54
|
+
printJsonError(COMMAND, { code, message });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
p.log.error(message);
|
|
58
|
+
p.outro(humanTail);
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
}
|
|
61
|
+
async function pickService(services) {
|
|
62
|
+
if (services.length === 0) {
|
|
63
|
+
p.log.error("The catalogue is empty — pass the service name explicitly: nustack add <service>");
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
if (!isInteractive()) {
|
|
67
|
+
p.log.error("No terminal for the picker — pass the service name explicitly: nustack add <service>");
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const selection = await p.select({
|
|
71
|
+
message: "Which service do you want to add?",
|
|
72
|
+
options: services.map((service) => ({
|
|
73
|
+
value: service.name,
|
|
74
|
+
label: service.name,
|
|
75
|
+
hint: [service.type ?? service.kind, service.status].filter(Boolean).join(" · "),
|
|
76
|
+
})),
|
|
77
|
+
});
|
|
78
|
+
if (p.isCancel(selection)) {
|
|
79
|
+
p.cancel("add cancelled.");
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return selection;
|
|
83
|
+
}
|
|
84
|
+
async function maybeInstallPackage(entry, cwd, json, yes) {
|
|
85
|
+
const packageName = entry?.npm?.package;
|
|
86
|
+
if (!packageName)
|
|
87
|
+
return false;
|
|
88
|
+
const manager = detectPackageManager(cwd);
|
|
89
|
+
const shouldInstall = json ? yes : await confirmOrFallback(`Install ${packageName} with ${manager}?`, true);
|
|
90
|
+
if (!shouldInstall) {
|
|
91
|
+
if (!json)
|
|
92
|
+
p.log.info(`Skipped the package install — run: ${manager} add ${packageName}`);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
await installPackage(manager, packageName, cwd);
|
|
97
|
+
if (!json)
|
|
98
|
+
p.log.success(`Installed ${packageName}`);
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (!json)
|
|
103
|
+
p.log.warn(`Package install failed (non-blocking): ${error instanceof Error ? error.message : error}`);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function recordConsumesEdge(entry, serviceName, cwd, json, yes) {
|
|
108
|
+
if (!hasManifest(cwd)) {
|
|
109
|
+
const repoName = path.basename(cwd).toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
|
110
|
+
const shouldScaffold = json
|
|
111
|
+
? yes
|
|
112
|
+
: await confirmOrFallback(`No nustack.service.json — scaffold one for "${repoName}" to record the consumes edge?`, true);
|
|
113
|
+
if (!shouldScaffold) {
|
|
114
|
+
if (!json)
|
|
115
|
+
p.log.info("Skipped — the consumes edge (bill of materials) was not recorded.");
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
await scaffoldManifest(cwd, repoName);
|
|
119
|
+
if (!json)
|
|
120
|
+
p.log.success("Scaffolded nustack.service.json");
|
|
121
|
+
}
|
|
122
|
+
const via = entry?.npm?.package ? "npm" : "http";
|
|
123
|
+
const wasAppended = await appendConsumesEdge(cwd, { service: serviceName, via, endpoint: "dev" });
|
|
124
|
+
if (!json) {
|
|
125
|
+
p.log.success(wasAppended
|
|
126
|
+
? `Recorded the consumes edge: ${serviceName} (via ${via})`
|
|
127
|
+
: `Consumes edge for ${serviceName} already declared`);
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { installSessionStartHook, removeSessionStartHook } from "../lib/agent/claudeHooks.js";
|
|
5
|
+
const PROBE_TIMEOUT_MS = 1500;
|
|
6
|
+
function desktopConfigDir() {
|
|
7
|
+
return (process.env.NUSTACK_DESKTOP_CONFIG_DIR ||
|
|
8
|
+
path.join(os.homedir(), "Library", "Application Support", "co.nurix.nustack"));
|
|
9
|
+
}
|
|
10
|
+
async function readServerRecord() {
|
|
11
|
+
const recordPath = path.join(desktopConfigDir(), "server.json");
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(await readFile(recordPath, "utf8"));
|
|
14
|
+
if (typeof parsed.port !== "number" ||
|
|
15
|
+
typeof parsed.token !== "string" ||
|
|
16
|
+
typeof parsed.pid !== "number" ||
|
|
17
|
+
typeof parsed.startedAt !== "string") {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return parsed;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isPidAlive(pid) {
|
|
27
|
+
try {
|
|
28
|
+
process.kill(pid, 0);
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return error.code === "EPERM";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function isRosterProject(value) {
|
|
36
|
+
return (value !== null &&
|
|
37
|
+
typeof value === "object" &&
|
|
38
|
+
typeof value.path === "string" &&
|
|
39
|
+
typeof value.label === "string");
|
|
40
|
+
}
|
|
41
|
+
async function probeProjects(record) {
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetch(`http://127.0.0.1:${record.port}/api/projects`, {
|
|
44
|
+
headers: { Authorization: `Bearer ${record.token}` },
|
|
45
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
46
|
+
});
|
|
47
|
+
if (!response.ok)
|
|
48
|
+
return null;
|
|
49
|
+
const body = (await response.json());
|
|
50
|
+
const projects = body.data?.projects;
|
|
51
|
+
return Array.isArray(projects) ? projects.filter(isRosterProject) : null;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function describeContext(record, projects) {
|
|
58
|
+
const baseUrl = `http://127.0.0.1:${record.port}`;
|
|
59
|
+
const roster = projects.length
|
|
60
|
+
? projects.map((project) => `- ${project.label} (${project.path})`).join("\n")
|
|
61
|
+
: "(no projects open)";
|
|
62
|
+
return [
|
|
63
|
+
`The NuStack desktop is running and reachable at ${baseUrl}.`,
|
|
64
|
+
`Bearer token for its API: ${record.token}`,
|
|
65
|
+
`Open projects (${projects.length}):`,
|
|
66
|
+
roster,
|
|
67
|
+
"This confirms NuStack answered just now — it says nothing about whether any project's code graph is current; ask `nustack graph` for that.",
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
export async function runAgentContext() {
|
|
71
|
+
const record = await readServerRecord();
|
|
72
|
+
if (!record)
|
|
73
|
+
return;
|
|
74
|
+
if (!isPidAlive(record.pid))
|
|
75
|
+
return;
|
|
76
|
+
const projects = await probeProjects(record);
|
|
77
|
+
if (projects === null)
|
|
78
|
+
return;
|
|
79
|
+
console.log(JSON.stringify({
|
|
80
|
+
hookSpecificOutput: {
|
|
81
|
+
hookEventName: "SessionStart",
|
|
82
|
+
additionalContext: describeContext(record, projects),
|
|
83
|
+
},
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
function claudeSettingsPath() {
|
|
87
|
+
return path.join(os.homedir(), ".claude", "settings.json");
|
|
88
|
+
}
|
|
89
|
+
export async function runAgentHookInstall() {
|
|
90
|
+
const { changed } = await installSessionStartHook(claudeSettingsPath());
|
|
91
|
+
console.log(changed
|
|
92
|
+
? "Installed the SessionStart hook."
|
|
93
|
+
: "Installed the SessionStart hook (already installed — no change).");
|
|
94
|
+
}
|
|
95
|
+
export async function runAgentHookRemove() {
|
|
96
|
+
const { removed } = await removeSessionStartHook(claudeSettingsPath());
|
|
97
|
+
console.log(removed
|
|
98
|
+
? "Removed the SessionStart hook."
|
|
99
|
+
: "Removed the SessionStart hook (already absent — no change).");
|
|
100
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
3
|
+
import { isInteractive } from "../lib/interactive.js";
|
|
4
|
+
import { ensureDeviceTelemetry } from "../lib/session_telemetry.js";
|
|
5
|
+
import { openLocalStore } from "../lib/local_store/database.js";
|
|
6
|
+
import { getBindingByRepositoryRoot } from "../lib/local_store/bindings.js";
|
|
7
|
+
import { listApplicationsForBinding } from "../lib/local_store/handoff_applications.js";
|
|
8
|
+
import { classifyFolder } from "../lib/onboarding/folder_state.js";
|
|
9
|
+
import { ensureSignedIn, runOnboardingFlow } from "./init.js";
|
|
10
|
+
export async function runBootstrap(options) {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const baseUrl = resolveNuStackUrl(options);
|
|
13
|
+
p.intro("nustack");
|
|
14
|
+
if (!isInteractive()) {
|
|
15
|
+
p.log.error("Bare `nustack` needs an interactive terminal (browser sign-in + prompts).");
|
|
16
|
+
p.outro("In scripted contexts run `nustack login --no-open`, then `nustack init`.");
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const accessToken = await ensureSignedIn(baseUrl, cwd);
|
|
21
|
+
if (!accessToken)
|
|
22
|
+
return;
|
|
23
|
+
await ensureDeviceTelemetry({ baseUrl, log: (message) => p.log.info(message) });
|
|
24
|
+
const state = classifyFolder(cwd);
|
|
25
|
+
const bound = state.class === "git_worktree" && state.repository && state.git?.markerWorkspaceId
|
|
26
|
+
? await printBoundStatus(state.repository.repositoryRoot)
|
|
27
|
+
: false;
|
|
28
|
+
if (bound)
|
|
29
|
+
return;
|
|
30
|
+
await runOnboardingFlow(baseUrl, accessToken, {});
|
|
31
|
+
}
|
|
32
|
+
async function printBoundStatus(root) {
|
|
33
|
+
const store = await openLocalStore();
|
|
34
|
+
try {
|
|
35
|
+
const binding = getBindingByRepositoryRoot(store, root);
|
|
36
|
+
if (!binding)
|
|
37
|
+
return false;
|
|
38
|
+
const pending = listApplicationsForBinding(store, binding.id, [
|
|
39
|
+
"received",
|
|
40
|
+
"evaluating",
|
|
41
|
+
"technical_wait",
|
|
42
|
+
"decision_required",
|
|
43
|
+
"approved",
|
|
44
|
+
]);
|
|
45
|
+
p.log.success(`This repository is bound to Workspace ${binding.workspaceId} on branch ${binding.boundBranch}.`);
|
|
46
|
+
p.log.info(`Binding ${binding.id} · ${binding.status} · applied Workspace sequence ${binding.lastAppliedWorkspaceSequence}` +
|
|
47
|
+
(pending.length > 0 ? ` · ${pending.length} pending inbound` : ""));
|
|
48
|
+
p.outro("Up to date locally — `nustack workspace status` for the cloud view, `nustack workspace sync now` to reconcile.");
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
store.close();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { fetchCatalogue, resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
3
|
+
export async function runDescribe(serviceName, options, globals) {
|
|
4
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
5
|
+
const catalogue = await fetchCatalogue(baseUrl);
|
|
6
|
+
const entry = (catalogue.services ?? []).find((service) => service.name === serviceName);
|
|
7
|
+
if (!entry) {
|
|
8
|
+
p.log.error(`'${serviceName}' is not in the catalogue at ${baseUrl} (${catalogue.services?.length ?? 0} services listed).`);
|
|
9
|
+
process.exitCode = 1;
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (options.json) {
|
|
13
|
+
console.log(JSON.stringify(entry, null, 2));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
console.log(`${entry.name} — ${entry.description ?? "(no description)"}`);
|
|
17
|
+
console.log(` kind/type: ${entry.kind ?? "?"} / ${entry.type ?? "?"}`);
|
|
18
|
+
console.log(` status: ${entry.status ?? "?"}`);
|
|
19
|
+
console.log(` environment: ${entry.environmentKey ?? "dev"}`);
|
|
20
|
+
if (entry.repository)
|
|
21
|
+
console.log(` repository: ${entry.repository}`);
|
|
22
|
+
if (entry.npm?.package)
|
|
23
|
+
console.log(` npm: ${entry.npm.package}`);
|
|
24
|
+
for (const [envName, url] of Object.entries(entry.http?.endpoints ?? {})) {
|
|
25
|
+
console.log(` endpoint: ${envName} → ${url}`);
|
|
26
|
+
}
|
|
27
|
+
if (entry.env?.required?.length)
|
|
28
|
+
console.log(` env (required): ${entry.env.required.join(", ")}`);
|
|
29
|
+
if (entry.env?.optional?.length)
|
|
30
|
+
console.log(` env (optional): ${entry.env.optional.join(", ")}`);
|
|
31
|
+
for (const edge of entry.consumes ?? []) {
|
|
32
|
+
console.log(` consumes: ${edge.service}${edge.via ? ` (via ${edge.via})` : ""}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { listDevices, resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
3
|
+
import { readGrant, revokeGrant } from "../lib/auth_grant.js";
|
|
4
|
+
import { ensureAuthWithRecovery, lastAuthFailure } from "../lib/auth_recovery.js";
|
|
5
|
+
import { deleteDeviceCredential } from "../lib/device_keychain.js";
|
|
6
|
+
import { isInteractive } from "../lib/interactive.js";
|
|
7
|
+
import { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
8
|
+
function clientLabel(client) {
|
|
9
|
+
if (client === "nustack-cli")
|
|
10
|
+
return "NuStack CLI";
|
|
11
|
+
if (client === "nustack-desktop")
|
|
12
|
+
return "NuStack desktop";
|
|
13
|
+
if (client === "browser")
|
|
14
|
+
return "Browser";
|
|
15
|
+
return client;
|
|
16
|
+
}
|
|
17
|
+
function relativeTime(iso) {
|
|
18
|
+
const then = Date.parse(iso);
|
|
19
|
+
if (Number.isNaN(then))
|
|
20
|
+
return iso;
|
|
21
|
+
const seconds = Math.max(0, Math.round((Date.now() - then) / 1000));
|
|
22
|
+
if (seconds < 60)
|
|
23
|
+
return `${seconds}s ago`;
|
|
24
|
+
const minutes = Math.round(seconds / 60);
|
|
25
|
+
if (minutes < 60)
|
|
26
|
+
return `${minutes}m ago`;
|
|
27
|
+
const hours = Math.round(minutes / 60);
|
|
28
|
+
if (hours < 48)
|
|
29
|
+
return `${hours}h ago`;
|
|
30
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
31
|
+
}
|
|
32
|
+
function formatRow(device) {
|
|
33
|
+
const name = device.machine ?? clientLabel(device.client);
|
|
34
|
+
const platform = device.platform && device.platform !== name ? device.platform : "";
|
|
35
|
+
return (`${device.deviceRef.padEnd(38)} ${name.padEnd(22)} ${clientLabel(device.client).padEnd(16)} ${platform.padEnd(10)} ` +
|
|
36
|
+
`active ${device.lastActiveAt ? relativeTime(device.lastActiveAt) : "never"}${device.isCurrent ? " (this machine)" : ""}`);
|
|
37
|
+
}
|
|
38
|
+
function toView(device, currentDeviceRef) {
|
|
39
|
+
return {
|
|
40
|
+
id: device.id,
|
|
41
|
+
deviceRef: device.deviceRef,
|
|
42
|
+
client: device.clientName ?? "unknown",
|
|
43
|
+
machine: device.label ?? device.displayName ?? null,
|
|
44
|
+
platform: device.platform ?? null,
|
|
45
|
+
lastActiveAt: device.lastSeenAt ?? null,
|
|
46
|
+
isCurrent: device.deviceRef === currentDeviceRef,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function failSignedOut(command, json) {
|
|
50
|
+
const failure = lastAuthFailure();
|
|
51
|
+
if (json)
|
|
52
|
+
printJsonError(command, { code: failure.code, message: failure.message });
|
|
53
|
+
else {
|
|
54
|
+
p.log.error(failure.message);
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function fail(command, json, code, message) {
|
|
59
|
+
if (json)
|
|
60
|
+
printJsonError(command, { code, message });
|
|
61
|
+
else {
|
|
62
|
+
p.log.error(message);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function liveGrant(baseUrl, json) {
|
|
67
|
+
const auth = await ensureAuthWithRecovery(baseUrl, json);
|
|
68
|
+
if (!auth)
|
|
69
|
+
return null;
|
|
70
|
+
const grant = await readGrant();
|
|
71
|
+
return grant ? { grant, accessToken: auth.accessToken } : null;
|
|
72
|
+
}
|
|
73
|
+
export async function runDeviceList(options, globals) {
|
|
74
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
75
|
+
const json = options.json === true;
|
|
76
|
+
const live = await liveGrant(baseUrl, json);
|
|
77
|
+
if (!live)
|
|
78
|
+
return failSignedOut("device list", json);
|
|
79
|
+
const devices = (await listDevices(baseUrl, live.accessToken)).map((row) => toView(row, live.grant.deviceId));
|
|
80
|
+
if (json) {
|
|
81
|
+
printJsonSuccess("device list", { devices });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (devices.length === 0) {
|
|
85
|
+
console.log("No devices signed in.");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
console.log(`${"DEVICE".padEnd(38)} ${"MACHINE".padEnd(22)} ${"CLIENT".padEnd(16)} ${"PLATFORM".padEnd(10)} LAST ACTIVE`);
|
|
89
|
+
for (const device of devices)
|
|
90
|
+
console.log(formatRow(device));
|
|
91
|
+
}
|
|
92
|
+
export async function runDeviceStatus(options, globals) {
|
|
93
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
94
|
+
const json = options.json === true;
|
|
95
|
+
const live = await liveGrant(baseUrl, json);
|
|
96
|
+
if (!live)
|
|
97
|
+
return failSignedOut("device status", json);
|
|
98
|
+
const { grant, accessToken } = live;
|
|
99
|
+
const rows = await listDevices(baseUrl, accessToken);
|
|
100
|
+
const current = rows.map((row) => toView(row, grant.deviceId)).find((d) => d.isCurrent) ?? null;
|
|
101
|
+
if (json) {
|
|
102
|
+
printJsonSuccess("device status", {
|
|
103
|
+
deviceId: grant.deviceId,
|
|
104
|
+
email: grant.userEmail,
|
|
105
|
+
authBaseUrl: grant.authBaseUrl,
|
|
106
|
+
resources: grant.grantedResources,
|
|
107
|
+
keychainMaterial: true,
|
|
108
|
+
server: current,
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
p.intro("nustack device status");
|
|
113
|
+
p.note([
|
|
114
|
+
`device id: ${grant.deviceId}`,
|
|
115
|
+
`signed in as: ${grant.userEmail || "—"}`,
|
|
116
|
+
`auth service: ${grant.authBaseUrl}`,
|
|
117
|
+
`resources: ${grant.grantedResources.join(", ") || "—"}`,
|
|
118
|
+
`keychain grant: present`,
|
|
119
|
+
`server record: ${current ? `${current.id} (${clientLabel(current.client)})` : "not found"}`,
|
|
120
|
+
`last active: ${current?.lastActiveAt ?? "—"}`,
|
|
121
|
+
].join("\n"), "This device");
|
|
122
|
+
p.outro(grant.userEmail || "signed in");
|
|
123
|
+
}
|
|
124
|
+
export async function runDeviceRevoke(deviceRefArg, options, globals) {
|
|
125
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
126
|
+
const json = options.json === true;
|
|
127
|
+
const live = await liveGrant(baseUrl, json);
|
|
128
|
+
if (!live)
|
|
129
|
+
return failSignedOut("device revoke", json);
|
|
130
|
+
const { grant, accessToken } = live;
|
|
131
|
+
const views = (await listDevices(baseUrl, accessToken)).map((row) => toView(row, grant.deviceId));
|
|
132
|
+
const target = deviceRefArg ? views.find((view) => view.deviceRef === deviceRefArg || view.id === deviceRefArg) : (views.find((view) => view.isCurrent) ?? null);
|
|
133
|
+
if (deviceRefArg && !target)
|
|
134
|
+
return fail("device revoke", json, "DEVICE_NOT_FOUND", "No device with that id — pass one from `nustack device list`.");
|
|
135
|
+
if (target && !target.isCurrent) {
|
|
136
|
+
const label = target.machine ? `${clientLabel(target.client)} on ${target.machine}` : clientLabel(target.client);
|
|
137
|
+
return fail("device revoke", json, "NOT_THIS_MACHINE", `Only this machine's own access can be ended from here. Sign ${label} out from that machine, or revoke it from Profile › Devices.`);
|
|
138
|
+
}
|
|
139
|
+
if (options.yes !== true) {
|
|
140
|
+
if (json || !isInteractive())
|
|
141
|
+
return fail("device revoke", json, "CONFIRMATION_REQUIRED", "Refusing to revoke without confirmation — re-run with --yes.");
|
|
142
|
+
const answer = await p.confirm({ message: "Revoke this machine's access? The CLI and the desktop here will be signed out." });
|
|
143
|
+
if (p.isCancel(answer) || !answer) {
|
|
144
|
+
p.outro("Cancelled — nothing revoked.");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const clientId = grant.clientId;
|
|
149
|
+
const outcome = clientId ? await revokeGrant({ authBaseUrl: grant.authBaseUrl, refreshToken: grant.grantToken, clientId }) : "unreachable";
|
|
150
|
+
if (outcome === "refused" || outcome === "unreachable") {
|
|
151
|
+
return fail("device revoke", json, "REVOKE_FAILED", outcome === "refused" ? "The auth service refused to end the grant." : "Couldn't reach the auth service to end the grant.");
|
|
152
|
+
}
|
|
153
|
+
await deleteDeviceCredential(grant.deviceId).catch(() => false);
|
|
154
|
+
if (json) {
|
|
155
|
+
printJsonSuccess("device revoke", { deviceId: grant.deviceId, revoked: true, current: true });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
p.log.success("Revoked this machine's access.");
|
|
159
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { getDocument, listDocuments, 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 { printJsonError, printJsonSuccess } from "../lib/json_output.js";
|
|
5
|
+
const INLINE_BODY_LIMIT = 16 * 1024;
|
|
6
|
+
async function resolveContext(command, json, options, globals) {
|
|
7
|
+
const baseUrl = resolveNuStackUrl(globals);
|
|
8
|
+
const workspace = await resolveWorkspaceId(process.cwd(), options);
|
|
9
|
+
if (!workspace.ok) {
|
|
10
|
+
emitFail(command, json, "WORKSPACE_CONTEXT_MISSING", workspace.message);
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const deviceAuth = await ensureAuthWithRecovery(baseUrl, json);
|
|
14
|
+
if (!deviceAuth) {
|
|
15
|
+
const failure = lastAuthFailure();
|
|
16
|
+
emitFail(command, json, failure.code, failure.message);
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return { baseUrl, accessToken: deviceAuth.accessToken, workspaceId: workspace.workspaceId };
|
|
20
|
+
}
|
|
21
|
+
function emitFail(command, json, code, message) {
|
|
22
|
+
if (json)
|
|
23
|
+
printJsonError(command, { code, message });
|
|
24
|
+
else {
|
|
25
|
+
console.error(message);
|
|
26
|
+
process.exitCode = 1;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function runDocsList(options, globals) {
|
|
30
|
+
const json = options.json === true;
|
|
31
|
+
const ctx = await resolveContext("docs list", json, options, globals);
|
|
32
|
+
if (!ctx)
|
|
33
|
+
return;
|
|
34
|
+
const documents = await listDocuments(ctx.baseUrl, ctx.accessToken, ctx.workspaceId);
|
|
35
|
+
if (json) {
|
|
36
|
+
printJsonSuccess("docs list", { workspaceId: ctx.workspaceId, documents });
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (documents.length === 0) {
|
|
40
|
+
console.log("This Workspace has no Documents yet.");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const doc of documents) {
|
|
44
|
+
const written = doc.hasBody ? "written" : "empty";
|
|
45
|
+
console.log(`${doc.slug.padEnd(28)} ${(doc.type ?? "-").padEnd(16)} ${doc.status.padEnd(8)} ${written.padEnd(8)} ${doc.label}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function runDocsShow(target, options, globals) {
|
|
49
|
+
const json = options.json === true;
|
|
50
|
+
const ctx = await resolveContext("docs show", json, options, globals);
|
|
51
|
+
if (!ctx)
|
|
52
|
+
return;
|
|
53
|
+
const documents = await listDocuments(ctx.baseUrl, ctx.accessToken, ctx.workspaceId);
|
|
54
|
+
const match = documents.find((d) => d.id === target || d.slug === target);
|
|
55
|
+
if (!match) {
|
|
56
|
+
emitFail("docs show", json, "DOCUMENT_NOT_FOUND", `No Document "${target}" in this Workspace.`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const payload = await getDocument(ctx.baseUrl, ctx.accessToken, match.id);
|
|
60
|
+
const doc = payload.document;
|
|
61
|
+
const body = doc.body || null;
|
|
62
|
+
const byteSize = body ? Buffer.byteLength(body) : 0;
|
|
63
|
+
const inlineable = body !== null && byteSize <= INLINE_BODY_LIMIT;
|
|
64
|
+
if (json) {
|
|
65
|
+
printJsonSuccess("docs show", {
|
|
66
|
+
document: {
|
|
67
|
+
id: doc.id,
|
|
68
|
+
slug: doc.slug,
|
|
69
|
+
label: doc.label,
|
|
70
|
+
documentType: doc.docType,
|
|
71
|
+
status: doc.status,
|
|
72
|
+
bodySha256: doc.bodySha256,
|
|
73
|
+
blockedBy: doc.blockedBy,
|
|
74
|
+
},
|
|
75
|
+
body: inlineable ? body : null,
|
|
76
|
+
byteSize,
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
console.log(`# ${doc.label} (${doc.slug})`);
|
|
81
|
+
console.log(`type: ${doc.docType ?? "-"} status: ${doc.status} bodySha256: ${doc.bodySha256.slice(0, 12)}`);
|
|
82
|
+
if (doc.blockedBy.length > 0)
|
|
83
|
+
console.log(`blocked by: ${doc.blockedBy.join(", ")}`);
|
|
84
|
+
if (inlineable && body !== null) {
|
|
85
|
+
console.log("");
|
|
86
|
+
console.log(body);
|
|
87
|
+
}
|
|
88
|
+
else if (body !== null) {
|
|
89
|
+
console.log(`Body is ${byteSize} bytes — larger than the inline limit; open it in the studio.`);
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log("Nothing has been written into this document yet.");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
export async function runDocsStatus(options, globals) {
|
|
96
|
+
const json = options.json === true;
|
|
97
|
+
const ctx = await resolveContext("docs status", json, options, globals);
|
|
98
|
+
if (!ctx)
|
|
99
|
+
return;
|
|
100
|
+
const documents = await listDocuments(ctx.baseUrl, ctx.accessToken, ctx.workspaceId);
|
|
101
|
+
const rows = documents.map((doc) => ({
|
|
102
|
+
slug: doc.slug,
|
|
103
|
+
documentId: doc.id,
|
|
104
|
+
bodySha256: doc.bodySha256,
|
|
105
|
+
status: doc.status,
|
|
106
|
+
convergence: "unmapped",
|
|
107
|
+
}));
|
|
108
|
+
if (json) {
|
|
109
|
+
printJsonSuccess("docs status", { workspaceId: ctx.workspaceId, paths: rows });
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (rows.length === 0) {
|
|
113
|
+
console.log("This Workspace has no Documents to converge.");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
for (const row of rows) {
|
|
117
|
+
console.log(`${row.slug.padEnd(28)} ${row.convergence.padEnd(10)} ${row.status.padEnd(8)} ${row.bodySha256 ? row.bodySha256.slice(0, 12) : "-"}`);
|
|
118
|
+
}
|
|
119
|
+
}
|