@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/dist/index.js
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { runLogin } from "./commands/login.js";
|
|
5
|
+
import { runInit } from "./commands/init.js";
|
|
6
|
+
import { runAdd } from "./commands/add.js";
|
|
7
|
+
import { runList } from "./commands/list.js";
|
|
8
|
+
import { runSync } from "./commands/sync.js";
|
|
9
|
+
import { runDescribe } from "./commands/describe.js";
|
|
10
|
+
import { runValidate } from "./commands/validate.js";
|
|
11
|
+
import { runGraph } from "./commands/graph.js";
|
|
12
|
+
import { runDoctor } from "./commands/doctor.js";
|
|
13
|
+
import { runOpen } from "./commands/open.js";
|
|
14
|
+
import { runBootstrap } from "./commands/bootstrap.js";
|
|
15
|
+
import { runLogout } from "./commands/logout.js";
|
|
16
|
+
import { runTelemetry } from "./commands/telemetry.js";
|
|
17
|
+
import { runSearch, runSearchServices } from "./commands/search.js";
|
|
18
|
+
import { runDeviceList, runDeviceRevoke, runDeviceStatus } from "./commands/device.js";
|
|
19
|
+
import { registerWorkspaceSyncCommands } from "./commands/workspace-sync.js";
|
|
20
|
+
import { registerWorkspaceCommands } from "./commands/workspace.js";
|
|
21
|
+
import { runOrganizationList, runOrganizationCreate, runOrganizationUse } from "./commands/organization.js";
|
|
22
|
+
import { runDocsList, runDocsShow, runDocsStatus } from "./commands/docs.js";
|
|
23
|
+
import { runHandoffList, runHandoffShow } from "./commands/handoff.js";
|
|
24
|
+
import { runToolkitStatus, runToolkitReconcile } from "./commands/toolkit.js";
|
|
25
|
+
import { runLaneServe } from "./commands/lane.js";
|
|
26
|
+
import { runSessionsRecord } from "./commands/sessions.js";
|
|
27
|
+
import { registerReviewCommands } from "./commands/review.js";
|
|
28
|
+
import { runAgentContext, runAgentHookInstall, runAgentHookRemove } from "./commands/agent.js";
|
|
29
|
+
import { printJsonError } from "./lib/json_output.js";
|
|
30
|
+
import { setCliVersion } from "./lib/auth_grant.js";
|
|
31
|
+
const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
32
|
+
setCliVersion(version);
|
|
33
|
+
const program = new Command();
|
|
34
|
+
program
|
|
35
|
+
.name("nustack")
|
|
36
|
+
.description("Bootstrap NuStack services into your repo via the NuStack discovery plane.")
|
|
37
|
+
.version(version)
|
|
38
|
+
.option("--nustack-url <url>", "NuStack base URL (defaults to $NUSTACK_URL, then the hosted instance)")
|
|
39
|
+
.action(async () => runBootstrap(program.opts()));
|
|
40
|
+
program.addHelpText("after", "\nRun `nustack` with no command to bootstrap: it checks your sign-in, runs login if needed, then init.");
|
|
41
|
+
program
|
|
42
|
+
.command("login")
|
|
43
|
+
.description("Sign in with your browser; this machine's grant is saved to your OS keychain")
|
|
44
|
+
.option("--no-open", "don't auto-open the browser; just print the sign-in URL")
|
|
45
|
+
.option("--manual", "sign in anywhere and paste the code here instead of using a browser on this machine")
|
|
46
|
+
.option("--force", "sign in again even if this machine already holds a grant, and ask for the account")
|
|
47
|
+
.action(async (options) => runLogin(program.opts(), options));
|
|
48
|
+
program
|
|
49
|
+
.command("logout")
|
|
50
|
+
.description("Sign out: end this machine's grant and clear the shared keychain credential (both CLI and desktop)")
|
|
51
|
+
.option("--forget-device", "also clear the shared ~/.nustack/device_id machine identity")
|
|
52
|
+
.action(async (options) => runLogout(program.opts(), { forgetDevice: options.forgetDevice }));
|
|
53
|
+
program
|
|
54
|
+
.command("init")
|
|
55
|
+
.description("Onboard this folder: sign in, then bind it to a Workspace (the onboarding router)")
|
|
56
|
+
.option("--organization <id>", "the Organization to create or select the Workspace in")
|
|
57
|
+
.option("--yes", "pre-confirm the populated-folder onboarding guard")
|
|
58
|
+
.action(async (options) => runInit({ ...program.opts(), ...options }));
|
|
59
|
+
program
|
|
60
|
+
.command("add [service]")
|
|
61
|
+
.description("Record a stack service in this repo: install its package, add the consumes edge")
|
|
62
|
+
.option("--json", "emit the versioned machine envelope instead of interactive output")
|
|
63
|
+
.option("--yes", "assume yes for the package-install and manifest-scaffold prompts")
|
|
64
|
+
.action(async (service, options) => runAdd(service, { ...program.opts(), ...options }));
|
|
65
|
+
program
|
|
66
|
+
.command("list")
|
|
67
|
+
.description("List the services in the NuStack catalogue")
|
|
68
|
+
.option("--json", "emit the versioned machine envelope wrapping the catalogue entries")
|
|
69
|
+
.action(async (options) => runList(options, program.opts()));
|
|
70
|
+
program
|
|
71
|
+
.command("sync")
|
|
72
|
+
.description("Project this Workspace's Service credentials into the managed .env block (not Documents or Git)")
|
|
73
|
+
.option("--workspace <id>", "the Workspace to act as (defaults to nustack.service.json / NUSTACK_WORKSPACE_ID)")
|
|
74
|
+
.option("--json", "emit the versioned machine envelope instead of interactive output")
|
|
75
|
+
.action(async (options) => runSync({ ...program.opts(), ...options }));
|
|
76
|
+
program
|
|
77
|
+
.command("describe <service>")
|
|
78
|
+
.description("Show the full catalogue entry for a service")
|
|
79
|
+
.option("--json", "emit the raw catalogue entry as JSON")
|
|
80
|
+
.action(async (service, options) => runDescribe(service, options, program.opts()));
|
|
81
|
+
program
|
|
82
|
+
.command("validate [manifest]")
|
|
83
|
+
.description("Validate an nustack.service.json (path or URL) against the nustack.dev/v1 schema")
|
|
84
|
+
.option("--schema <pathOrUrl>", "override the bundled packages/contracts/spec/schema.json")
|
|
85
|
+
.option("--json", "emit the versioned machine envelope with { target, isValid, errors }")
|
|
86
|
+
.action(async (manifest, options) => runValidate(manifest, options));
|
|
87
|
+
program
|
|
88
|
+
.command("graph [fn] [args...]")
|
|
89
|
+
.description("The code graph: ask a question of this repository's index, or `index` / `status` / `functions`")
|
|
90
|
+
.option("--repo <pathOrId>", "the repository to ask about (default: the working tree's root)")
|
|
91
|
+
.option("--no-refresh", "answer from the index as it stands, however old")
|
|
92
|
+
.option("--budget <n>", "token ceiling for the answer; 0 means unbounded")
|
|
93
|
+
.option("--commits <n>", "history depth for `graph index` (default 5000)")
|
|
94
|
+
.option("--json", "emit the versioned machine envelope with the answer as data")
|
|
95
|
+
.allowUnknownOption()
|
|
96
|
+
.allowExcessArguments()
|
|
97
|
+
.action(async (_fn, _args, options) => runGraph(process.argv, options));
|
|
98
|
+
program
|
|
99
|
+
.command("doctor")
|
|
100
|
+
.description("Run the full local diagnostics matrix (identity, repository, manifests, toolkit, telemetry, credentials)")
|
|
101
|
+
.option("--workspace <id>", "the Workspace to diagnose (defaults to the bound repository)")
|
|
102
|
+
.option("--json", "emit the versioned machine envelope (secrets and absolute paths redacted)")
|
|
103
|
+
.option("--local-verbose", "include Device-local absolute paths in the output")
|
|
104
|
+
.action(async (options) => runDoctor({ ...program.opts(), ...options }));
|
|
105
|
+
program
|
|
106
|
+
.command("open <service>")
|
|
107
|
+
.description("Open the service's dev endpoint (or repository) in the browser")
|
|
108
|
+
.option("--env <name>", "pick a specific endpoint environment (default: dev)")
|
|
109
|
+
.option("--json", "print the resolved URL as the machine envelope instead of launching")
|
|
110
|
+
.action(async (service, options) => runOpen(service, options, program.opts()));
|
|
111
|
+
const device = program.command("device").description("Inspect your signed-in devices and end this machine's access");
|
|
112
|
+
device
|
|
113
|
+
.command("list")
|
|
114
|
+
.description("List your signed-in devices (never a token)")
|
|
115
|
+
.option("--json", "emit the versioned machine envelope")
|
|
116
|
+
.action(async (options) => runDeviceList(options, program.opts()));
|
|
117
|
+
device
|
|
118
|
+
.command("status")
|
|
119
|
+
.description("Show this device's local state merged with its server record")
|
|
120
|
+
.option("--json", "emit the versioned machine envelope")
|
|
121
|
+
.action(async (options) => runDeviceStatus(options, program.opts()));
|
|
122
|
+
device
|
|
123
|
+
.command("revoke [device-id]")
|
|
124
|
+
.description("End this machine's access (other machines are revoked from Profile › Devices)")
|
|
125
|
+
.option("--json", "emit the versioned machine envelope")
|
|
126
|
+
.option("--yes", "skip the confirmation prompt")
|
|
127
|
+
.action(async (deviceId, options) => runDeviceRevoke(deviceId, options, program.opts()));
|
|
128
|
+
program
|
|
129
|
+
.command("telemetry [action]")
|
|
130
|
+
.description("Inspect or control telemetry (status | enable | disable | reinstall). Inside a bound folder, enable/disable move that folder alone")
|
|
131
|
+
.option("--json", "emit the versioned machine envelope")
|
|
132
|
+
.option("--device", "move this machine's switch instead of the current folder's")
|
|
133
|
+
.option("--local-verbose", "include each folder's absolute path (local consumers only — never pipe into a log)")
|
|
134
|
+
.action(async (action, options) => {
|
|
135
|
+
const allowed = ["status", "enable", "disable", "reinstall"];
|
|
136
|
+
const chosen = (allowed.includes(action ?? "") ? action : "status");
|
|
137
|
+
return runTelemetry(chosen, {
|
|
138
|
+
...program.opts(),
|
|
139
|
+
json: options.json,
|
|
140
|
+
device: options.device,
|
|
141
|
+
localVerbose: options.localVerbose,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
const toLimit = (value) => Number.parseInt(value, 10);
|
|
145
|
+
const search = program
|
|
146
|
+
.command("search [keywords...]")
|
|
147
|
+
.description("Search the NuStack Directory for workspaces by keyword")
|
|
148
|
+
.option("--json", "emit the raw results array as JSON")
|
|
149
|
+
.option("--category <cat>", "filter workspaces by category")
|
|
150
|
+
.option("--limit <n>", "max results (default 20)", toLimit)
|
|
151
|
+
.action(async (keywords, options) => runSearch(keywords, options, program.opts()));
|
|
152
|
+
search
|
|
153
|
+
.command("services [keywords...]")
|
|
154
|
+
.description("Search the services registry by keyword (then `nustack add <name>` to integrate)")
|
|
155
|
+
.option("--json", "emit the raw results array as JSON")
|
|
156
|
+
.option("--type <type>", "filter services by type")
|
|
157
|
+
.option("--limit <n>", "max results (default 20)", toLimit)
|
|
158
|
+
.action(async (keywords, options) => runSearchServices(keywords, { ...search.opts(), ...options }, program.opts()));
|
|
159
|
+
search.addHelpText("after", '\n`services` is a reserved subcommand. To search workspaces for the literal word "services", use the\n' +
|
|
160
|
+
"explicit separator: `nustack search -- services` (everything after `--` is an workspaces query).");
|
|
161
|
+
const organization = program.command("organization").description("Work with your NuStack Organizations");
|
|
162
|
+
organization
|
|
163
|
+
.command("list")
|
|
164
|
+
.description("List your Organization memberships (name, slug, role, active marker)")
|
|
165
|
+
.option("--json", "emit the versioned machine envelope")
|
|
166
|
+
.action(async (options) => runOrganizationList(options, program.opts()));
|
|
167
|
+
organization
|
|
168
|
+
.command("create <name>")
|
|
169
|
+
.description("Create an Organization and print it")
|
|
170
|
+
.option("--json", "emit the versioned machine envelope")
|
|
171
|
+
.action(async (name, options) => runOrganizationCreate(name, options, program.opts()));
|
|
172
|
+
organization
|
|
173
|
+
.command("use <id-or-slug>")
|
|
174
|
+
.description("Set the active Organization context for this device")
|
|
175
|
+
.option("--json", "emit the versioned machine envelope")
|
|
176
|
+
.action(async (target, options) => runOrganizationUse(target, options, program.opts()));
|
|
177
|
+
const workspace = program.command("workspace").description("Work with NuStack Workspaces");
|
|
178
|
+
registerWorkspaceCommands(workspace, () => program.opts());
|
|
179
|
+
registerWorkspaceSyncCommands(workspace, () => program.opts());
|
|
180
|
+
registerReviewCommands(program.command("review").description("Read and mirror NuStack Review items"), () => program.opts());
|
|
181
|
+
const docs = program.command("docs").description("Work with a Workspace's Documents");
|
|
182
|
+
docs
|
|
183
|
+
.command("list")
|
|
184
|
+
.description("List the Workspace's Documents")
|
|
185
|
+
.option("--workspace <id>", "the Workspace to act as (defaults to the bound repository)")
|
|
186
|
+
.option("--json", "emit the versioned machine envelope")
|
|
187
|
+
.action(async (options) => runDocsList(options, program.opts()));
|
|
188
|
+
docs
|
|
189
|
+
.command("show <slug-or-id>")
|
|
190
|
+
.description("Show a Document: a bounded inline body or its content URL")
|
|
191
|
+
.option("--workspace <id>", "the Workspace to act as (defaults to the bound repository)")
|
|
192
|
+
.option("--json", "emit the versioned machine envelope")
|
|
193
|
+
.action(async (target, options) => runDocsShow(target, options, program.opts()));
|
|
194
|
+
docs
|
|
195
|
+
.command("status")
|
|
196
|
+
.description("Report per-path local/cloud Document convergence for the bound repository")
|
|
197
|
+
.option("--workspace <id>", "the Workspace to act as (defaults to the bound repository)")
|
|
198
|
+
.option("--json", "emit the versioned machine envelope")
|
|
199
|
+
.action(async (options) => runDocsStatus(options, program.opts()));
|
|
200
|
+
const handoff = program.command("handoff").description("Work with a Workspace's Handoffs");
|
|
201
|
+
handoff
|
|
202
|
+
.command("list")
|
|
203
|
+
.description("List the Workspace's Handoffs")
|
|
204
|
+
.option("--workspace <id>", "the Workspace to act as (defaults to the bound repository)")
|
|
205
|
+
.option("--json", "emit the versioned machine envelope")
|
|
206
|
+
.action(async (options) => runHandoffList(options, program.opts()));
|
|
207
|
+
handoff
|
|
208
|
+
.command("show <id>")
|
|
209
|
+
.description("Show a Handoff (add --targets for the binding-scoped per-target manifest)")
|
|
210
|
+
.option("--workspace <id>", "the Workspace to act as (defaults to the bound repository)")
|
|
211
|
+
.option("--targets", "list each target's path, operation, and base semantics")
|
|
212
|
+
.option("--json", "emit the versioned machine envelope")
|
|
213
|
+
.action(async (id, options) => runHandoffShow(id, options, program.opts()));
|
|
214
|
+
const lane = program.command("lane").description("The desktop build room's lane engine");
|
|
215
|
+
lane
|
|
216
|
+
.command("serve")
|
|
217
|
+
.description("Run the long-lived lane supervisor: JSONL frames on stdin/stdout (used by the NuStack desktop)")
|
|
218
|
+
.action(async () => runLaneServe(version));
|
|
219
|
+
const sessions = program.command("sessions").description("The session record over this machine's agent work");
|
|
220
|
+
sessions
|
|
221
|
+
.command("record")
|
|
222
|
+
.description("Record the sessions one desktop scan found in a bound folder (used by the NuStack desktop)")
|
|
223
|
+
.option("--scan <json>", "the scan payload as JSON, or @<path> to read it from a file")
|
|
224
|
+
.option("--json", "emit the versioned machine envelope")
|
|
225
|
+
.action(async (options) => runSessionsRecord(options, program.opts()));
|
|
226
|
+
const toolkit = program.command("toolkit").description("Inspect and reconcile the Workspace's toolkit pin");
|
|
227
|
+
toolkit
|
|
228
|
+
.command("status")
|
|
229
|
+
.description("Compare the cloud pin against .nustack/workspace.json and nustack.service.json (F-026)")
|
|
230
|
+
.option("--workspace <id>", "the Workspace to act as (defaults to the bound repository)")
|
|
231
|
+
.option("--json", "emit the versioned machine envelope")
|
|
232
|
+
.action(async (options) => runToolkitStatus(options, program.opts()));
|
|
233
|
+
toolkit
|
|
234
|
+
.command("reconcile")
|
|
235
|
+
.description("Preview and apply the deterministic toolkit repair (no User content is touched)")
|
|
236
|
+
.option("--workspace <id>", "the Workspace to act as (defaults to the bound repository)")
|
|
237
|
+
.option("--yes", "apply the deterministic preview without prompting")
|
|
238
|
+
.option("--json", "emit the versioned machine envelope")
|
|
239
|
+
.action(async (options) => runToolkitReconcile(options, program.opts()));
|
|
240
|
+
const agent = program.command("agent").description("The discovery hook an agent uses to find the running NuStack desktop");
|
|
241
|
+
agent
|
|
242
|
+
.command("context")
|
|
243
|
+
.description("Print the SessionStart hook payload when the NuStack desktop is reachable (silent otherwise)")
|
|
244
|
+
.action(async () => runAgentContext());
|
|
245
|
+
const agentHook = agent.command("hook").description("Manage the `nustack agent context` SessionStart hook in ~/.claude/settings.json");
|
|
246
|
+
agentHook
|
|
247
|
+
.command("install")
|
|
248
|
+
.description("Register the SessionStart hook (idempotent — a second install is a no-op)")
|
|
249
|
+
.action(async () => runAgentHookInstall());
|
|
250
|
+
agentHook
|
|
251
|
+
.command("remove")
|
|
252
|
+
.description("Remove exactly the managed SessionStart hook entry")
|
|
253
|
+
.action(async () => runAgentHookRemove());
|
|
254
|
+
function describeError(error) {
|
|
255
|
+
const directCode = error?.code;
|
|
256
|
+
if (directCode === "WORKSPACE_CONTEXT_MISSING") {
|
|
257
|
+
return "no Workspace in context — run inside a bound repository (`.nustack/workspace.json`) or pass `--workspace <id>`.";
|
|
258
|
+
}
|
|
259
|
+
if (directCode === "ORGANIZATION_CONTEXT_MISSING") {
|
|
260
|
+
return "no Organization in context — run `nustack organization use <id>` or pass `--organization <id>`.";
|
|
261
|
+
}
|
|
262
|
+
if (directCode === "TOOLKIT_INCOMPATIBLE") {
|
|
263
|
+
return "the installed toolkit is incompatible with the Workspace's cloud pin — run `nustack toolkit reconcile`.";
|
|
264
|
+
}
|
|
265
|
+
const cause = error?.cause;
|
|
266
|
+
const code = cause?.code;
|
|
267
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN") {
|
|
268
|
+
return "couldn't resolve the NuStack host — check your connection or pass --nustack-url.";
|
|
269
|
+
}
|
|
270
|
+
if (code === "ECONNREFUSED") {
|
|
271
|
+
return "the NuStack host refused the connection — is it up? Override with --nustack-url.";
|
|
272
|
+
}
|
|
273
|
+
if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) {
|
|
274
|
+
return "the request to NuStack timed out — check your connection or try again.";
|
|
275
|
+
}
|
|
276
|
+
if (error instanceof Error && error.message === "fetch failed") {
|
|
277
|
+
return "couldn't reach NuStack — check your connection or pass --nustack-url.";
|
|
278
|
+
}
|
|
279
|
+
return error instanceof Error ? error.message : String(error);
|
|
280
|
+
}
|
|
281
|
+
function searchSeparatorInvocation(rawArgv) {
|
|
282
|
+
const args = rawArgv.slice(2);
|
|
283
|
+
const searchIdx = args.indexOf("search");
|
|
284
|
+
if (searchIdx === -1)
|
|
285
|
+
return null;
|
|
286
|
+
if (searchIdx > 0 && args[searchIdx - 1] === "--nustack-url")
|
|
287
|
+
return null;
|
|
288
|
+
let cut = searchIdx + 1;
|
|
289
|
+
while (cut < args.length) {
|
|
290
|
+
const token = args[cut] ?? "";
|
|
291
|
+
if (token === "--category" || token === "--limit" || token === "--nustack-url") {
|
|
292
|
+
cut += 2;
|
|
293
|
+
}
|
|
294
|
+
else if (token.startsWith("--") && token !== "--") {
|
|
295
|
+
cut += 1;
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (args[cut] !== "--")
|
|
302
|
+
return null;
|
|
303
|
+
const beforeSeparator = args.slice(0, cut);
|
|
304
|
+
const literalKeywords = args.slice(cut + 1);
|
|
305
|
+
return {
|
|
306
|
+
run: async () => {
|
|
307
|
+
const scoped = new Command();
|
|
308
|
+
scoped.exitOverride();
|
|
309
|
+
scoped.name("nustack").option("--nustack-url <url>", "");
|
|
310
|
+
scoped
|
|
311
|
+
.command("search [keywords...]")
|
|
312
|
+
.option("--json", "")
|
|
313
|
+
.option("--category <cat>", "")
|
|
314
|
+
.option("--limit <n>", "", toLimit)
|
|
315
|
+
.action(async (keywords, options) => runSearch(keywords, options, scoped.opts()));
|
|
316
|
+
await scoped.parseAsync(["node", "nustack", ...beforeSeparator, ...literalKeywords]);
|
|
317
|
+
},
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function isJsonInvocation(argv) {
|
|
321
|
+
return argv.slice(2).includes("--json");
|
|
322
|
+
}
|
|
323
|
+
function commandLabelFromArgv(argv) {
|
|
324
|
+
const args = argv.slice(2);
|
|
325
|
+
const parts = [];
|
|
326
|
+
for (let i = 0; i < args.length; i++) {
|
|
327
|
+
const token = args[i] ?? "";
|
|
328
|
+
if (token === "--nustack-url") {
|
|
329
|
+
i++;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (token.startsWith("-"))
|
|
333
|
+
break;
|
|
334
|
+
parts.push(token);
|
|
335
|
+
}
|
|
336
|
+
return parts.length ? parts.join(" ") : "nustack";
|
|
337
|
+
}
|
|
338
|
+
function errorCodeOf(error) {
|
|
339
|
+
const direct = error?.code ?? error?.cause?.code;
|
|
340
|
+
return typeof direct === "string" ? direct : "UNEXPECTED";
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const separated = searchSeparatorInvocation(process.argv);
|
|
344
|
+
if (separated)
|
|
345
|
+
await separated.run();
|
|
346
|
+
else
|
|
347
|
+
await program.parseAsync(process.argv);
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
if (isJsonInvocation(process.argv)) {
|
|
351
|
+
printJsonError(commandLabelFromArgv(process.argv), { code: errorCodeOf(error), message: describeError(error) });
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
console.error(`nustack: ${describeError(error)}`);
|
|
355
|
+
process.exitCode = 1;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const MANAGED_MARKER = "nustack:managed";
|
|
5
|
+
const MANAGED_COMMAND = `nustack agent context # ${MANAGED_MARKER}`;
|
|
6
|
+
function managedEntry() {
|
|
7
|
+
return { matcher: "", hooks: [{ type: "command", command: MANAGED_COMMAND }] };
|
|
8
|
+
}
|
|
9
|
+
function isManagedEntry(entry) {
|
|
10
|
+
if (entry === null || typeof entry !== "object")
|
|
11
|
+
return false;
|
|
12
|
+
const hooks = entry.hooks;
|
|
13
|
+
if (!Array.isArray(hooks))
|
|
14
|
+
return false;
|
|
15
|
+
return hooks.some((hook) => hook !== null &&
|
|
16
|
+
typeof hook === "object" &&
|
|
17
|
+
typeof hook.command === "string" &&
|
|
18
|
+
hook.command.includes(MANAGED_MARKER));
|
|
19
|
+
}
|
|
20
|
+
const DEFAULT_INDENT = 2;
|
|
21
|
+
function detectIndent(raw) {
|
|
22
|
+
const indented = /\n(\x20+)"/.exec(raw);
|
|
23
|
+
return indented?.[1] ? indented[1].length : DEFAULT_INDENT;
|
|
24
|
+
}
|
|
25
|
+
async function readSettings(settingsPath) {
|
|
26
|
+
if (!existsSync(settingsPath))
|
|
27
|
+
return { settings: {}, indent: DEFAULT_INDENT };
|
|
28
|
+
const raw = await readFile(settingsPath, "utf8");
|
|
29
|
+
if (raw.trim() === "")
|
|
30
|
+
return { settings: {}, indent: DEFAULT_INDENT };
|
|
31
|
+
return { settings: JSON.parse(raw), indent: detectIndent(raw) };
|
|
32
|
+
}
|
|
33
|
+
async function writeSettings(settingsPath, settings, indent) {
|
|
34
|
+
await mkdir(path.dirname(settingsPath), { recursive: true });
|
|
35
|
+
await writeFile(settingsPath, `${JSON.stringify(settings, null, indent)}\n`, "utf8");
|
|
36
|
+
}
|
|
37
|
+
export async function installSessionStartHook(settingsPath) {
|
|
38
|
+
const { settings, indent } = await readSettings(settingsPath);
|
|
39
|
+
const hooks = { ...(settings.hooks ?? {}) };
|
|
40
|
+
const sessionStart = Array.isArray(hooks.SessionStart) ? [...hooks.SessionStart] : [];
|
|
41
|
+
const existingIndex = sessionStart.findIndex(isManagedEntry);
|
|
42
|
+
const entry = managedEntry();
|
|
43
|
+
if (existingIndex !== -1) {
|
|
44
|
+
const current = sessionStart[existingIndex];
|
|
45
|
+
if (JSON.stringify(current) === JSON.stringify(entry)) {
|
|
46
|
+
return { changed: false };
|
|
47
|
+
}
|
|
48
|
+
sessionStart[existingIndex] = entry;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
sessionStart.push(entry);
|
|
52
|
+
}
|
|
53
|
+
hooks.SessionStart = sessionStart;
|
|
54
|
+
settings.hooks = hooks;
|
|
55
|
+
await writeSettings(settingsPath, settings, indent);
|
|
56
|
+
return { changed: true };
|
|
57
|
+
}
|
|
58
|
+
export async function removeSessionStartHook(settingsPath) {
|
|
59
|
+
const { settings, indent } = await readSettings(settingsPath);
|
|
60
|
+
const hooks = settings.hooks;
|
|
61
|
+
const sessionStart = hooks?.SessionStart;
|
|
62
|
+
if (!hooks || !Array.isArray(sessionStart))
|
|
63
|
+
return { removed: false };
|
|
64
|
+
const filtered = sessionStart.filter((entry) => !isManagedEntry(entry));
|
|
65
|
+
if (filtered.length === sessionStart.length)
|
|
66
|
+
return { removed: false };
|
|
67
|
+
const nextHooks = { ...hooks };
|
|
68
|
+
if (filtered.length > 0) {
|
|
69
|
+
nextHooks.SessionStart = filtered;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
delete nextHooks.SessionStart;
|
|
73
|
+
}
|
|
74
|
+
settings.hooks = nextHooks;
|
|
75
|
+
await writeSettings(settingsPath, settings, indent);
|
|
76
|
+
return { removed: true };
|
|
77
|
+
}
|