@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.
Files changed (131) hide show
  1. package/README.md +77 -0
  2. package/dist/commands/add.js +130 -0
  3. package/dist/commands/agent.js +100 -0
  4. package/dist/commands/bootstrap.js +54 -0
  5. package/dist/commands/describe.js +34 -0
  6. package/dist/commands/device.js +159 -0
  7. package/dist/commands/docs.js +119 -0
  8. package/dist/commands/doctor.js +284 -0
  9. package/dist/commands/graph.js +34 -0
  10. package/dist/commands/handoff.js +108 -0
  11. package/dist/commands/init.js +280 -0
  12. package/dist/commands/lane.js +12 -0
  13. package/dist/commands/list.js +20 -0
  14. package/dist/commands/login.js +134 -0
  15. package/dist/commands/logout.js +44 -0
  16. package/dist/commands/open.js +54 -0
  17. package/dist/commands/organization.js +114 -0
  18. package/dist/commands/review.js +152 -0
  19. package/dist/commands/search.js +57 -0
  20. package/dist/commands/sessions.js +62 -0
  21. package/dist/commands/sync.js +85 -0
  22. package/dist/commands/telemetry.js +112 -0
  23. package/dist/commands/toolkit.js +167 -0
  24. package/dist/commands/validate.js +67 -0
  25. package/dist/commands/workspace-sync.js +645 -0
  26. package/dist/commands/workspace.js +479 -0
  27. package/dist/index.js +357 -0
  28. package/dist/lib/agent/claudeHooks.js +77 -0
  29. package/dist/lib/auth_grant.js +398 -0
  30. package/dist/lib/auth_recovery.js +49 -0
  31. package/dist/lib/browser_opener.js +24 -0
  32. package/dist/lib/device_identity.js +28 -0
  33. package/dist/lib/device_keychain.js +101 -0
  34. package/dist/lib/env_file.js +75 -0
  35. package/dist/lib/etna/manager.js +43 -0
  36. package/dist/lib/etna/profile.js +41 -0
  37. package/dist/lib/etna/reconcile.js +77 -0
  38. package/dist/lib/filebeat_installer.js +246 -0
  39. package/dist/lib/git-sync/core/commit_range.js +37 -0
  40. package/dist/lib/git-sync/core/engine.js +263 -0
  41. package/dist/lib/git-sync/core/git_runner.js +137 -0
  42. package/dist/lib/git-sync/core/operation_state.js +22 -0
  43. package/dist/lib/git-sync/core/path_normalizer.js +98 -0
  44. package/dist/lib/git-sync/core/reconciliation_lock.js +103 -0
  45. package/dist/lib/git-sync/core/repository.js +49 -0
  46. package/dist/lib/git-sync/core/watch_stream.js +86 -0
  47. package/dist/lib/git-sync/core/watcher.js +61 -0
  48. package/dist/lib/git-sync/inbound/applier.js +131 -0
  49. package/dist/lib/git-sync/inbound/apply_commit.js +225 -0
  50. package/dist/lib/git-sync/inbound/approval.js +117 -0
  51. package/dist/lib/git-sync/inbound/content_decision.js +148 -0
  52. package/dist/lib/git-sync/inbound/download.js +223 -0
  53. package/dist/lib/git-sync/inbound/event_sync.js +133 -0
  54. package/dist/lib/git-sync/inbound/index.js +10 -0
  55. package/dist/lib/git-sync/inbound/notification.js +39 -0
  56. package/dist/lib/git-sync/inbound/state_fingerprint.js +32 -0
  57. package/dist/lib/git-sync/inbound/technical_wait.js +39 -0
  58. package/dist/lib/git-sync/inbound/undo.js +198 -0
  59. package/dist/lib/git-sync/outbound/extraction.js +107 -0
  60. package/dist/lib/git-sync/outbound/history_decision.js +112 -0
  61. package/dist/lib/git-sync/outbound/index.js +26 -0
  62. package/dist/lib/git-sync/outbound/manifest_builder.js +58 -0
  63. package/dist/lib/git-sync/outbound/publisher.js +51 -0
  64. package/dist/lib/git-sync/outbound/queue.js +123 -0
  65. package/dist/lib/git-sync/outbound/state.js +12 -0
  66. package/dist/lib/git-sync/outbound/upload_client.js +141 -0
  67. package/dist/lib/git-sync/recovery/bundle.js +56 -0
  68. package/dist/lib/git-sync/recovery/crypto.js +114 -0
  69. package/dist/lib/git-sync/recovery/index.js +3 -0
  70. package/dist/lib/git-sync/recovery/store.js +55 -0
  71. package/dist/lib/git-sync/whitelist/index.js +4 -0
  72. package/dist/lib/git-sync/whitelist/matcher.js +62 -0
  73. package/dist/lib/git-sync/whitelist/store.js +99 -0
  74. package/dist/lib/git-sync/whitelist/types.js +46 -0
  75. package/dist/lib/git-sync/whitelist/validation.js +60 -0
  76. package/dist/lib/graph/answer.js +171 -0
  77. package/dist/lib/graph/freshness.js +29 -0
  78. package/dist/lib/graph/library.js +19 -0
  79. package/dist/lib/guards.js +106 -0
  80. package/dist/lib/interactive.js +12 -0
  81. package/dist/lib/json_output.js +8 -0
  82. package/dist/lib/lanes/claudeAdapter.js +135 -0
  83. package/dist/lib/lanes/frames.js +73 -0
  84. package/dist/lib/lanes/supervisor.js +247 -0
  85. package/dist/lib/lanes/worktree.js +33 -0
  86. package/dist/lib/local_store/accepted_uploads.js +49 -0
  87. package/dist/lib/local_store/bindings.js +163 -0
  88. package/dist/lib/local_store/commit_origins.js +25 -0
  89. package/dist/lib/local_store/database.js +71 -0
  90. package/dist/lib/local_store/device_state.js +72 -0
  91. package/dist/lib/local_store/filebeat_state.js +133 -0
  92. package/dist/lib/local_store/handoff_applications.js +166 -0
  93. package/dist/lib/local_store/history_decisions.js +48 -0
  94. package/dist/lib/local_store/index.js +14 -0
  95. package/dist/lib/local_store/migrations.js +240 -0
  96. package/dist/lib/local_store/paths.js +24 -0
  97. package/dist/lib/local_store/pending_uploads.js +76 -0
  98. package/dist/lib/local_store/recovery_bundles.js +66 -0
  99. package/dist/lib/local_store/sync_vocabulary.js +33 -0
  100. package/dist/lib/local_store/workspace_events_cache.js +54 -0
  101. package/dist/lib/loopback.js +78 -0
  102. package/dist/lib/manifest.js +83 -0
  103. package/dist/lib/nustack_client.js +547 -0
  104. package/dist/lib/nustack_store.js +17 -0
  105. package/dist/lib/onboarding/attach_existing_git.js +65 -0
  106. package/dist/lib/onboarding/binding.js +91 -0
  107. package/dist/lib/onboarding/folder_state.js +99 -0
  108. package/dist/lib/onboarding/git_init.js +10 -0
  109. package/dist/lib/onboarding/handoff_apply.js +68 -0
  110. package/dist/lib/onboarding/init_fresh_folder.js +100 -0
  111. package/dist/lib/onboarding/manifest_writer.js +161 -0
  112. package/dist/lib/onboarding/web_first_attach.js +133 -0
  113. package/dist/lib/package_manager.js +23 -0
  114. package/dist/lib/review/client.js +66 -0
  115. package/dist/lib/schema_validator.js +24 -0
  116. package/dist/lib/secure_file.js +10 -0
  117. package/dist/lib/session_telemetry.js +263 -0
  118. package/dist/lib/sessions/client.js +48 -0
  119. package/dist/lib/sessions/outsideSessions.js +54 -0
  120. package/dist/lib/sse_framing.js +48 -0
  121. package/dist/lib/telemetry/config_renderer.js +175 -0
  122. package/dist/lib/telemetry/health_report.js +173 -0
  123. package/dist/lib/telemetry/provider_paths.js +38 -0
  124. package/dist/lib/telemetry/redaction_policy.js +90 -0
  125. package/dist/lib/telemetry/reload.js +84 -0
  126. package/dist/lib/workspace_context.js +121 -0
  127. package/dist/spec_schema.json +422 -0
  128. package/dist/wire/auth.js +1 -0
  129. package/dist/wire/sync-paths.js +25 -0
  130. package/dist/wire/telemetry.js +1 -0
  131. package/package.json +49 -0
@@ -0,0 +1,43 @@
1
+ import { detectPackageManager, installPackage } from "../package_manager.js";
2
+ import { assertSafePackageName } from "../guards.js";
3
+ import { updateBindingEtnaProfile } from "../local_store/bindings.js";
4
+ import { serializeProfileForBinding } from "./profile.js";
5
+ export function toolkitPackageName(profile) {
6
+ if (!profile.name.startsWith("@"))
7
+ return null;
8
+ try {
9
+ return assertSafePackageName(profile.name);
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ export async function installToolkit(store, bindingId, profile, cwd, deps = {}) {
16
+ const packageName = toolkitPackageName(profile);
17
+ let result;
18
+ if (!packageName) {
19
+ result = { ok: true, installed: false, skipped: true, reason: "device_level_toolkit" };
20
+ }
21
+ else {
22
+ const observed = deps.observe ? await deps.observe(profile, cwd) : { name: profile.name, version: null, installed: false };
23
+ if (observed.installed && observed.version === profile.version) {
24
+ result = { ok: true, installed: false, skipped: true, reason: "already_pinned" };
25
+ }
26
+ else {
27
+ const manager = deps.manager ?? detectPackageManager(cwd);
28
+ const install = deps.install ?? installPackage;
29
+ try {
30
+ await install(manager, packageName, cwd);
31
+ result = { ok: true, installed: true, skipped: false };
32
+ }
33
+ catch (error) {
34
+ result = { ok: false, installed: false, skipped: false, reason: error instanceof Error ? error.message : String(error) };
35
+ }
36
+ }
37
+ }
38
+ updateBindingEtnaProfile(store, bindingId, serializeProfileForBinding(profile));
39
+ return result;
40
+ }
41
+ export function verifyToolkit(profile, observed) {
42
+ return observed.installed && observed.name === profile.name && observed.version === profile.version;
43
+ }
@@ -0,0 +1,41 @@
1
+ import { getWorkspace } from "../nustack_client.js";
2
+ export class ToolkitProfileUnavailableError extends Error {
3
+ workspaceId;
4
+ constructor(workspaceId) {
5
+ super("The Workspace summary did not project a Toolkit Profile pin — the owning server plan must expose it on GET /api/v2/workspaces/{workspaceId}.");
6
+ this.name = "ToolkitProfileUnavailableError";
7
+ this.workspaceId = workspaceId;
8
+ }
9
+ }
10
+ export async function fetchToolkitProfile(baseUrl, accessToken, workspaceId) {
11
+ const workspace = await getWorkspace(baseUrl, accessToken, workspaceId);
12
+ const toolkit = workspace.toolkit;
13
+ if (!toolkit || typeof toolkit.name !== "string" || typeof toolkit.version !== "string") {
14
+ throw new ToolkitProfileUnavailableError(workspaceId);
15
+ }
16
+ return {
17
+ name: toolkit.name,
18
+ version: toolkit.version,
19
+ rendererVersion: typeof toolkit.rendererVersion === "string" ? toolkit.rendererVersion : "",
20
+ placementMapVersion: typeof toolkit.placementMapVersion === "string" ? toolkit.placementMapVersion : "",
21
+ catalogVersionId: null,
22
+ };
23
+ }
24
+ export function toolkitProfileFromManifest(manifest) {
25
+ return {
26
+ name: manifest.toolkit.name,
27
+ version: manifest.toolkit.version,
28
+ rendererVersion: manifest.toolkit.rendererVersion,
29
+ placementMapVersion: "",
30
+ catalogVersionId: null,
31
+ };
32
+ }
33
+ export function serializeProfileForBinding(profile) {
34
+ return JSON.stringify({
35
+ name: profile.name,
36
+ version: profile.version,
37
+ rendererVersion: profile.rendererVersion,
38
+ placementMapVersion: profile.placementMapVersion,
39
+ catalogVersion: profile.catalogVersionId,
40
+ });
41
+ }
@@ -0,0 +1,77 @@
1
+ import { writeWorkspaceManifests, readWorkspaceManifests, diagnoseManifests, } from "../onboarding/manifest_writer.js";
2
+ import { serializeProfileForBinding } from "./profile.js";
3
+ import { updateBindingEtnaProfile } from "../local_store/bindings.js";
4
+ export function parseRecordedToolkit(binding) {
5
+ if (!binding.etnaProfileJson)
6
+ return null;
7
+ try {
8
+ const p = JSON.parse(binding.etnaProfileJson);
9
+ if (typeof p.name !== "string" || typeof p.version !== "string")
10
+ return null;
11
+ return {
12
+ name: p.name,
13
+ version: p.version,
14
+ rendererVersion: typeof p.rendererVersion === "string" ? p.rendererVersion : "",
15
+ placementMapVersion: typeof p.placementMapVersion === "string" ? p.placementMapVersion : "",
16
+ };
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ export function toolkitCompatibility(binding, cloud) {
23
+ const recorded = parseRecordedToolkit(binding);
24
+ if (!recorded || !cloud)
25
+ return { compatible: true, blocksWrites: false, reason: null };
26
+ if (recorded.name !== cloud.name) {
27
+ return { compatible: false, blocksWrites: true, reason: `toolkit name diverges (local ${recorded.name} vs cloud ${cloud.name})` };
28
+ }
29
+ if (recorded.version !== cloud.version) {
30
+ return { compatible: false, blocksWrites: true, reason: `toolkit version diverges (local ${recorded.version} vs cloud ${cloud.version})` };
31
+ }
32
+ if (cloud.rendererVersion && recorded.rendererVersion && recorded.rendererVersion !== cloud.rendererVersion) {
33
+ return { compatible: false, blocksWrites: true, reason: `renderer version diverges (local ${recorded.rendererVersion} vs cloud ${cloud.rendererVersion})` };
34
+ }
35
+ return { compatible: true, blocksWrites: false, reason: null };
36
+ }
37
+ export async function buildReconcilePlan(root, binding, cloud) {
38
+ const manifests = await readWorkspaceManifests(root);
39
+ const cloudId = { name: cloud.name, version: cloud.version };
40
+ const diagnosis = diagnoseManifests(cloudId, manifests.workspaceManifest?.toolkit ?? null, manifests.serviceToolkit);
41
+ const changes = [];
42
+ const workspace = manifests.workspaceManifest?.toolkit;
43
+ if (workspace) {
44
+ pushIf(changes, ".nustack/workspace.json", "toolkit.name", workspace.name, cloud.name);
45
+ pushIf(changes, ".nustack/workspace.json", "toolkit.version", workspace.version, cloud.version);
46
+ pushIf(changes, ".nustack/workspace.json", "toolkit.rendererVersion", workspace.rendererVersion, cloud.rendererVersion);
47
+ pushIf(changes, ".nustack/workspace.json", "toolkit.placementMapVersion", workspace.placementMapVersion, cloud.placementMapVersion);
48
+ }
49
+ const svc = manifests.serviceToolkit;
50
+ if (svc) {
51
+ pushIf(changes, "nustack.service.json", "toolkit.name", svc.name, cloud.name);
52
+ pushIf(changes, "nustack.service.json", "toolkit.version", svc.version, cloud.version);
53
+ }
54
+ const recorded = parseRecordedToolkit(binding);
55
+ if (recorded) {
56
+ pushIf(changes, "binding", "name", recorded.name, cloud.name);
57
+ pushIf(changes, "binding", "version", recorded.version, cloud.version);
58
+ }
59
+ return { diagnosis, changes, migrations: [], noOp: changes.length === 0 && diagnosis === "aligned" };
60
+ }
61
+ function pushIf(changes, file, field, from, to) {
62
+ if (from !== to)
63
+ changes.push({ file, field, from, to });
64
+ }
65
+ export async function applyReconcile(store, bindingId, root, identity, cloud) {
66
+ await writeWorkspaceManifests(root, {
67
+ workspaceId: identity.workspaceId,
68
+ organizationId: identity.organizationId,
69
+ toolkit: {
70
+ name: cloud.name,
71
+ version: cloud.version,
72
+ rendererVersion: cloud.rendererVersion,
73
+ placementMapVersion: cloud.placementMapVersion,
74
+ },
75
+ });
76
+ updateBindingEtnaProfile(store, bindingId, serializeProfileForBinding(cloud));
77
+ }
@@ -0,0 +1,246 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash } from "node:crypto";
3
+ import { createRequire } from "node:module";
4
+ import { chmod, copyFile, mkdir, rm, writeFile } from "node:fs/promises";
5
+ import { existsSync } from "node:fs";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ import { nustackDir } from "./nustack_store.js";
9
+ const FILEBEAT_VERSION = process.env.NUSTACK_FILEBEAT_VERSION || "8.15.3";
10
+ export function desiredFilebeatVersion() {
11
+ return FILEBEAT_VERSION;
12
+ }
13
+ export function filebeatDir() {
14
+ return path.join(nustackDir(), "filebeat");
15
+ }
16
+ export function filebeatBinPath() {
17
+ return path.join(filebeatDir(), process.platform === "win32" ? "filebeat.exe" : "filebeat");
18
+ }
19
+ export function inputsDir() {
20
+ return path.join(filebeatDir(), "inputs.d");
21
+ }
22
+ export function mainConfigPath() {
23
+ return path.join(filebeatDir(), "filebeat.yml");
24
+ }
25
+ function artifactArch() {
26
+ if (process.platform === "linux")
27
+ return process.arch === "arm64" ? "arm64" : "x86_64";
28
+ return process.arch === "arm64" ? "aarch64" : "x86_64";
29
+ }
30
+ function artifactOs() {
31
+ if (process.platform === "darwin")
32
+ return "darwin";
33
+ if (process.platform === "win32")
34
+ return "windows";
35
+ return "linux";
36
+ }
37
+ function resolveBundledBinary() {
38
+ const pkg = `@nurix/nustack-filebeat-${process.platform}-${process.arch}`;
39
+ try {
40
+ const require = createRequire(import.meta.url);
41
+ const bin = process.platform === "win32" ? "filebeat.exe" : "filebeat";
42
+ return require.resolve(`${pkg}/${bin}`);
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ async function verifyChecksum(archive, checksumUrl, fileName) {
49
+ const response = await fetch(checksumUrl, { signal: AbortSignal.timeout(30_000) });
50
+ if (!response.ok) {
51
+ throw new Error(`Filebeat checksum fetch failed: HTTP ${response.status} (${checksumUrl})`);
52
+ }
53
+ const published = (await response.text()).trim().split(/\s+/)[0]?.toLowerCase();
54
+ const actual = createHash("sha512").update(archive).digest("hex");
55
+ if (!published || published !== actual) {
56
+ throw new Error(`Filebeat checksum mismatch for ${fileName} — refusing to install. Expected ${published}, got ${actual}.`);
57
+ }
58
+ }
59
+ async function downloadFilebeat(log) {
60
+ const ext = process.platform === "win32" ? "zip" : "tar.gz";
61
+ const stem = `filebeat-${FILEBEAT_VERSION}-${artifactOs()}-${artifactArch()}`;
62
+ const url = `https://artifacts.elastic.co/downloads/beats/filebeat/${stem}.${ext}`;
63
+ const tmp = path.join(filebeatDir(), `.${stem}.${ext}`);
64
+ log(`Downloading Filebeat ${FILEBEAT_VERSION} (${artifactOs()}-${artifactArch()})`);
65
+ const response = await fetch(url, { signal: AbortSignal.timeout(120_000) });
66
+ if (!response.ok)
67
+ throw new Error(`Filebeat download failed: HTTP ${response.status} (${url})`);
68
+ const archive = Buffer.from(await response.arrayBuffer());
69
+ await verifyChecksum(archive, `${url}.sha512`, `${stem}.${ext}`);
70
+ await mkdir(filebeatDir(), { recursive: true });
71
+ await writeFile(tmp, archive);
72
+ execFileSync("tar", ["-xf", tmp, "-C", filebeatDir()], { stdio: "ignore" });
73
+ await rm(tmp, { force: true });
74
+ const extractedBin = path.join(filebeatDir(), stem, process.platform === "win32" ? "filebeat.exe" : "filebeat");
75
+ await copyFile(extractedBin, filebeatBinPath());
76
+ if (process.platform !== "win32")
77
+ await chmod(filebeatBinPath(), 0o755);
78
+ await rm(path.join(filebeatDir(), stem), { recursive: true, force: true });
79
+ }
80
+ export async function ensureFilebeatBinary(log) {
81
+ if (existsSync(filebeatBinPath()))
82
+ return;
83
+ await mkdir(filebeatDir(), { recursive: true });
84
+ const bundled = resolveBundledBinary();
85
+ if (bundled) {
86
+ log("Using bundled Filebeat binary");
87
+ await copyFile(bundled, filebeatBinPath());
88
+ if (process.platform !== "win32")
89
+ await chmod(filebeatBinPath(), 0o755);
90
+ return;
91
+ }
92
+ await downloadFilebeat(log);
93
+ }
94
+ export function observedFilebeatVersion() {
95
+ if (!existsSync(filebeatBinPath()))
96
+ return null;
97
+ try {
98
+ const out = execFileSync(filebeatBinPath(), ["version"], {
99
+ encoding: "utf8",
100
+ timeout: 10_000,
101
+ stdio: ["ignore", "pipe", "ignore"],
102
+ });
103
+ return /version\s+(\d+\.\d+\.\d+)/.exec(out)?.[1] ?? null;
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ }
109
+ const SERVICE_LABEL = "co.nurix.nustack.filebeat";
110
+ function run(cmd, args) {
111
+ try {
112
+ execFileSync(cmd, args, { stdio: "ignore" });
113
+ return true;
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
119
+ function filebeatArgs() {
120
+ return [
121
+ "-c",
122
+ mainConfigPath(),
123
+ "--path.home",
124
+ filebeatDir(),
125
+ "--path.config",
126
+ filebeatDir(),
127
+ "--path.data",
128
+ path.join(filebeatDir(), "data"),
129
+ ];
130
+ }
131
+ async function registerLaunchAgent() {
132
+ const plistPath = path.join(os.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`);
133
+ const args = [filebeatBinPath(), ...filebeatArgs()]
134
+ .map((a) => ` <string>${a}</string>`)
135
+ .join("\n");
136
+ const logPath = path.join(filebeatDir(), "filebeat.log");
137
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
138
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
139
+ <plist version="1.0">
140
+ <dict>
141
+ <key>Label</key><string>${SERVICE_LABEL}</string>
142
+ <key>ProgramArguments</key>
143
+ <array>
144
+ ${args}
145
+ </array>
146
+ <key>RunAtLoad</key><true/>
147
+ <key>KeepAlive</key><true/>
148
+ <key>StandardOutPath</key><string>${logPath}</string>
149
+ <key>StandardErrorPath</key><string>${logPath}</string>
150
+ </dict>
151
+ </plist>
152
+ `;
153
+ await mkdir(path.dirname(plistPath), { recursive: true });
154
+ await writeFile(plistPath, plist, "utf8");
155
+ run("launchctl", ["unload", plistPath]);
156
+ return run("launchctl", ["load", "-w", plistPath]);
157
+ }
158
+ async function registerSystemdUser() {
159
+ const unitDir = path.join(os.homedir(), ".config", "systemd", "user");
160
+ const exec = [filebeatBinPath(), ...filebeatArgs()].join(" ");
161
+ const unit = `[Unit]
162
+ Description=NuStack session telemetry (Filebeat)
163
+
164
+ [Service]
165
+ ExecStart=${exec}
166
+ Restart=always
167
+
168
+ [Install]
169
+ WantedBy=default.target
170
+ `;
171
+ await mkdir(unitDir, { recursive: true });
172
+ await writeFile(path.join(unitDir, "nustack-filebeat.service"), unit, "utf8");
173
+ run("systemctl", ["--user", "daemon-reload"]);
174
+ return run("systemctl", ["--user", "enable", "--now", "nustack-filebeat.service"]);
175
+ }
176
+ function registerScheduledTask() {
177
+ const tr = [filebeatBinPath(), ...filebeatArgs()].map((a) => `"${a}"`).join(" ");
178
+ return run("schtasks", ["/Create", "/TN", "NuStackFilebeat", "/TR", tr, "/SC", "ONLOGON", "/F"]);
179
+ }
180
+ export async function registerAndStartService() {
181
+ await mkdir(path.join(filebeatDir(), "data"), { recursive: true });
182
+ if (process.platform === "darwin")
183
+ return registerLaunchAgent();
184
+ if (process.platform === "linux")
185
+ return registerSystemdUser();
186
+ if (process.platform === "win32")
187
+ return registerScheduledTask();
188
+ return false;
189
+ }
190
+ export async function restartService() {
191
+ if (process.platform === "darwin")
192
+ return registerLaunchAgent();
193
+ if (process.platform === "linux")
194
+ return run("systemctl", ["--user", "restart", "nustack-filebeat.service"]);
195
+ if (process.platform === "win32") {
196
+ run("schtasks", ["/End", "/TN", "NuStackFilebeat"]);
197
+ return run("schtasks", ["/Run", "/TN", "NuStackFilebeat"]);
198
+ }
199
+ return false;
200
+ }
201
+ export function unregisterService() {
202
+ if (process.platform === "darwin") {
203
+ run("launchctl", [
204
+ "unload",
205
+ "-w",
206
+ path.join(os.homedir(), "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`),
207
+ ]);
208
+ }
209
+ else if (process.platform === "linux") {
210
+ run("systemctl", ["--user", "disable", "--now", "nustack-filebeat.service"]);
211
+ }
212
+ else if (process.platform === "win32") {
213
+ run("schtasks", ["/Delete", "/TN", "NuStackFilebeat", "/F"]);
214
+ }
215
+ }
216
+ export function probeServiceRegistered() {
217
+ if (process.platform === "darwin")
218
+ return run("launchctl", ["list", SERVICE_LABEL]);
219
+ if (process.platform === "linux")
220
+ return run("systemctl", ["--user", "is-active", "nustack-filebeat.service"]);
221
+ if (process.platform === "win32")
222
+ return run("schtasks", ["/Query", "/TN", "NuStackFilebeat"]);
223
+ return false;
224
+ }
225
+ export function testFilebeatConfig(ctx) {
226
+ if (!existsSync(filebeatBinPath()))
227
+ return { ok: false, output: "filebeat binary not installed" };
228
+ try {
229
+ const output = execFileSync(filebeatBinPath(), [
230
+ "test",
231
+ "config",
232
+ "-c",
233
+ ctx.mainConfigPath,
234
+ "--path.home",
235
+ filebeatDir(),
236
+ "--path.config",
237
+ ctx.configDir,
238
+ "--path.data",
239
+ path.join(filebeatDir(), "data"),
240
+ ], { encoding: "utf8", timeout: 15_000, stdio: ["ignore", "pipe", "pipe"] });
241
+ return { ok: true, output };
242
+ }
243
+ catch (error) {
244
+ return { ok: false, output: error instanceof Error ? error.message : String(error) };
245
+ }
246
+ }
@@ -0,0 +1,37 @@
1
+ import { runGit } from "./git_runner.js";
2
+ import { readHeadCommit, readSymbolicBranch } from "./repository.js";
3
+ function firstParentChain(repo, head) {
4
+ const { stdout } = runGit(repo.repositoryRoot, ["rev-list", "--first-parent", head]);
5
+ return stdout
6
+ .split("\n")
7
+ .map((line) => line.trim())
8
+ .filter((line) => line.length > 0);
9
+ }
10
+ export function resolveCommitRange(repo, input) {
11
+ const branch = readSymbolicBranch(repo);
12
+ if (branch === null)
13
+ return { kind: "branch_wait", reason: "detached_head" };
14
+ if (branch !== input.boundBranch)
15
+ return { kind: "branch_wait", reason: "other_branch" };
16
+ const head = readHeadCommit(repo);
17
+ if (head === null)
18
+ return { kind: "no_op" };
19
+ if (input.lastProcessedCommitId === null)
20
+ return { kind: "no_op" };
21
+ if (input.lastProcessedCommitId === head)
22
+ return { kind: "no_op" };
23
+ if (!firstParentChain(repo, head).includes(input.lastProcessedCommitId)) {
24
+ return { kind: "history_decision_required" };
25
+ }
26
+ const { stdout } = runGit(repo.repositoryRoot, [
27
+ "rev-list",
28
+ "--first-parent",
29
+ "--reverse",
30
+ `${input.lastProcessedCommitId}..${head}`,
31
+ ]);
32
+ const commitIds = stdout
33
+ .split("\n")
34
+ .map((line) => line.trim())
35
+ .filter((line) => line.length > 0);
36
+ return { kind: "range", commitIds };
37
+ }