@wrongstack/cli 1.0.1 → 1.0.4

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 (109) hide show
  1. package/data/providers.json +21 -21
  2. package/dist/{acp-3LARESEO.js → acp-AB54FWM2.js} +122 -39
  3. package/dist/acp-agent-list.d.ts +14 -0
  4. package/dist/acp-hq-telemetry.d.ts +9 -2
  5. package/dist/acp-mcp-servers.d.ts +49 -0
  6. package/dist/acp-server-agent.d.ts +18 -6
  7. package/dist/{auth-ZUMCDNE5.js → auth-WDMSNZ3S.js} +79 -55
  8. package/dist/auth-menu/index.d.ts +1 -0
  9. package/dist/auth-menu/oauth-menu.d.ts +3 -1
  10. package/dist/auth-menu/panel-service.d.ts +2 -0
  11. package/dist/auth-menu/provider-auth-login.d.ts +12 -0
  12. package/dist/auth-menu/types.d.ts +3 -0
  13. package/dist/auto-discover-providers-DXR25GTG.js +7 -0
  14. package/dist/boot/dispatch-webui.d.ts +1 -0
  15. package/dist/boot/short-circuit-hq.d.ts +2 -0
  16. package/dist/boot/system-prompt-builder.d.ts +7 -0
  17. package/dist/boot/system-prompt.d.ts +2 -1
  18. package/dist/boot/tool-registry.d.ts +18 -1
  19. package/dist/chimera-reviewer-policy.d.ts +1 -1
  20. package/dist/{chunk-C2E24IXZ.js → chunk-3I5ZJXM5.js} +147 -241
  21. package/dist/{chunk-JHPXJRRI.js → chunk-4XCBEUBX.js} +13 -3
  22. package/dist/{chunk-FPIWTJ2G.js → chunk-6WRKAACA.js} +4 -4
  23. package/dist/chunk-AUQDGU5D.js +32 -0
  24. package/dist/{chunk-D2VCUN7M.js → chunk-BIWE22O5.js} +17 -17
  25. package/dist/{chunk-VOTTENR5.js → chunk-CDY4D5QK.js} +2 -2
  26. package/dist/chunk-D5GB5MHA.js +87 -0
  27. package/dist/chunk-DAKVBMD4.js +36 -0
  28. package/dist/{chunk-E6RFXHWD.js → chunk-EK2P53GL.js} +3 -2
  29. package/dist/chunk-I7V2OYVH.js +242 -0
  30. package/dist/{chunk-MXEJEYT7.js → chunk-JX6KL7OF.js} +15 -5
  31. package/dist/{chunk-ZSZ3EYWH.js → chunk-KEPABQYT.js} +109 -6
  32. package/dist/{chunk-IMWXXF4J.js → chunk-KNMIBKLV.js} +79 -1
  33. package/dist/{chunk-MNQQ4ZNF.js → chunk-ONOT5OJ2.js} +290 -687
  34. package/dist/chunk-Q7E3BPDU.js +62 -0
  35. package/dist/chunk-SIGUWXAA.js +43 -0
  36. package/dist/{chunk-TEBMCYZN.js → chunk-SIS37L3W.js} +58 -2
  37. package/dist/{chunk-KHWDTNBZ.js → chunk-U6PGWDMZ.js} +5 -5
  38. package/dist/{chunk-5IA5337I.js → chunk-XJ7K67PP.js} +8 -2
  39. package/dist/{cli-context-7X3M6U73.js → cli-context-WFC3BNGM.js} +131 -25
  40. package/dist/{cli-main-53KG6HAV.js → cli-main-GU3YZLMO.js} +3986 -2827
  41. package/dist/{diag-doctor-7LYUYU35.js → diag-doctor-PCER6KHH.js} +2 -2
  42. package/dist/execute-deps.d.ts +1 -0
  43. package/dist/{execution-MIBWBYB6.js → execution-RAZDDQFC.js} +115 -16
  44. package/dist/execution-specialist-trigger.d.ts +28 -0
  45. package/dist/fleet/acp-live-progress.d.ts +19 -0
  46. package/dist/fleet/host-acp-runner-cache.d.ts +3 -1
  47. package/dist/fleet/host-acp.d.ts +14 -2
  48. package/dist/fleet/host.d.ts +1 -0
  49. package/dist/{hq-IG3YRGCO.js → hq-5XHEIIH6.js} +127 -8
  50. package/dist/hq-command-controller.d.ts +31 -2
  51. package/dist/hq-server/client-address.d.ts +2 -0
  52. package/dist/hq-server/ip-allowlist.d.ts +16 -0
  53. package/dist/hq-server/preflight.d.ts +1 -0
  54. package/dist/hq-server/routes/auth/password-routes.d.ts +1 -1
  55. package/dist/hq-server/routes.d.ts +5 -4
  56. package/dist/hq-server/secret-input.d.ts +7 -0
  57. package/dist/hq-server/types.d.ts +3 -2
  58. package/dist/hq-server/upgrade-handler.d.ts +2 -0
  59. package/dist/hq-server/utils.d.ts +6 -0
  60. package/dist/{hq-server-OILU3NPI.js → hq-server-CNUTBWTK.js} +4 -3
  61. package/dist/hq-server.d.ts +4 -0
  62. package/dist/hq-service-L5INQFM4.js +507 -0
  63. package/dist/hq-service.d.ts +22 -0
  64. package/dist/index.js +4 -4
  65. package/dist/ip-allowlist-NHXO6AKJ.js +9 -0
  66. package/dist/live-settings-input.d.ts +4 -0
  67. package/dist/{mailbox-serve-VUCWZ2SJ.js → mailbox-serve-X3CR6VFW.js} +4 -3
  68. package/dist/{modeldiag-FMJT37SN.js → modeldiag-46CMWUNM.js} +31 -24
  69. package/dist/{per-subcommand-help-BWBHYVJO.js → per-subcommand-help-DHTYU524.js} +2 -2
  70. package/dist/{plugin-usage-HFBI3TFT.js → plugin-usage-M6KET4OZ.js} +2 -2
  71. package/dist/{plugins-PZ2VZL7V.js → plugins-WJLW6ULV.js} +3 -3
  72. package/dist/projects-LMNAGGEU.js +36 -0
  73. package/dist/{providers-models-YVMVYWRS.js → providers-models-6MOGAOVK.js} +2 -2
  74. package/dist/{proxy-wiring-MGRSKZZB.js → proxy-wiring-K2DORFFL.js} +3 -3
  75. package/dist/short-circuit-flags-YDMLAQ5R.js +10 -0
  76. package/dist/slash-commands/acp.d.ts +3 -0
  77. package/dist/slash-commands/codebase-map.d.ts +11 -0
  78. package/dist/slash-commands/command-context.d.ts +29 -0
  79. package/dist/slash-commands/context.d.ts +10 -0
  80. package/dist/slash-commands/ensemble.d.ts +1 -1
  81. package/dist/slash-commands/openai-quota.d.ts +31 -0
  82. package/dist/slash-commands/profile.d.ts +7 -0
  83. package/dist/subcommands/handlers/hq.d.ts +1 -0
  84. package/dist/subcommands/handlers/projects.d.ts +14 -0
  85. package/dist/{subcommands-WJLBHVKC.js → subcommands-7N4RIK6H.js} +2 -2
  86. package/dist/{version-help-MYKF724O.js → version-help-WXI42PZ3.js} +2 -2
  87. package/dist/{webui-server-5R35BHWZ.js → webui-server-OBV5JYJV.js} +27 -4
  88. package/dist/webui-server-options.d.ts +1 -0
  89. package/dist/wiring/atlas-prompt-contributor.d.ts +54 -0
  90. package/dist/wiring/cli-execute-builder.d.ts +1 -0
  91. package/dist/wiring/cli-slash-commands-setup.d.ts +7 -0
  92. package/dist/wiring/codebase-embeddings.d.ts +31 -0
  93. package/dist/wiring/command-host-state.d.ts +9 -0
  94. package/dist/wiring/concept-summarizer.d.ts +32 -0
  95. package/dist/wiring/external-plugins.d.ts +1 -0
  96. package/dist/wiring/lifecycle-plugins.d.ts +2 -1
  97. package/dist/wiring/pipeline.d.ts +2 -0
  98. package/dist/wiring/plugins.d.ts +2 -1
  99. package/dist/wiring/provider-runtime-setup.d.ts +1 -1
  100. package/dist/wiring/provider.d.ts +2 -1
  101. package/dist/wiring/session-command-handlers.d.ts +2 -1
  102. package/dist/wiring/slash-commands.d.ts +1 -0
  103. package/package.json +29 -29
  104. package/dist/auto-discover-providers-VQU3IZMR.js +0 -7
  105. package/dist/chunk-52C7PD2H.js +0 -61
  106. package/dist/chunk-Q5GTM25S.js +0 -17
  107. package/dist/chunk-TNK6237M.js +0 -262
  108. package/dist/projects-L6C6XLDY.js +0 -37
  109. package/dist/short-circuit-flags-UREFSYBL.js +0 -10
@@ -4,6 +4,7 @@ interface HqPreflightOptions {
4
4
  port?: number;
5
5
  dataDir?: string;
6
6
  password?: string;
7
+ bootstrapPasswordOnly?: boolean;
7
8
  tokenTtlMs?: number;
8
9
  requireBrowserAuth?: boolean;
9
10
  allowInsecureOpen?: boolean;
@@ -3,7 +3,7 @@ import type { LoginAttemptStore } from '../../login-attempt-store.js';
3
3
  import type { HqRouterMutableAuth, HqSessionEntry } from '../../types.js';
4
4
  import { type ApplyHqAuthFile } from './common.js';
5
5
  export declare function handleApiAuthStatus(req: http.IncomingMessage, res: http.ServerResponse, url: URL, mutableAuth: HqRouterMutableAuth, sessions: Map<string, HqSessionEntry>, requireBrowserAuth: boolean | undefined, trustedPublicOrigins: Set<string>, secureCookies: boolean | undefined): Promise<void>;
6
- export declare function handleApiLogin(req: http.IncomingMessage, res: http.ServerResponse, mutableAuth: HqRouterMutableAuth, sessions: Map<string, HqSessionEntry>, loginAttempts: LoginAttemptStore, secureCookies: boolean | undefined, trustedProxyHops: number): Promise<void>;
6
+ export declare function handleApiLogin(req: http.IncomingMessage, res: http.ServerResponse, mutableAuth: HqRouterMutableAuth, sessions: Map<string, HqSessionEntry>, loginAttempts: LoginAttemptStore, secureCookies: boolean | undefined, trustedProxyHops: number, sessionCapabilities?: string[]): Promise<void>;
7
7
  export declare function handleApiLogout(req: http.IncomingMessage, res: http.ServerResponse, mutableAuth: HqRouterMutableAuth, sessions: Map<string, HqSessionEntry>, secureCookies: boolean | undefined): Promise<void>;
8
8
  export declare function handleApiPassword(req: http.IncomingMessage, res: http.ServerResponse, mutableAuth: HqRouterMutableAuth, sessions: Map<string, HqSessionEntry>, dataDir: string, secureCookies: boolean | undefined, requireBrowserAuth: boolean | undefined, applyAuthFile: ApplyHqAuthFile): Promise<void>;
9
9
  //# sourceMappingURL=password-routes.d.ts.map
@@ -8,10 +8,8 @@
8
8
  * @module hq-server/routes
9
9
  */
10
10
  import type * as http from 'node:http';
11
- import type { createMailboxHttpRouter } from '@wrongstack/core/coordination';
12
- import type { Mailbox, MailboxHttpAccessDecision, MailboxHttpRateLimiter } from '@wrongstack/core/coordination';
13
- import type { HqAlertEngine, HqCommandAuditLog, HqEventEnvelope, HqSnapshot, HqTranscriptEntry } from '@wrongstack/core/hq';
14
- import type { createHqPersistence } from '@wrongstack/core/hq';
11
+ import type { createMailboxHttpRouter, Mailbox, MailboxHttpAccessDecision, MailboxHttpRateLimiter } from '@wrongstack/core/coordination';
12
+ import type { createHqPersistence, HqAlertEngine, HqCommandAuditLog, HqEventEnvelope, HqSnapshot, HqTranscriptEntry } from '@wrongstack/core/hq';
15
13
  import type { TrustBoundary } from '@wrongstack/core/security';
16
14
  import type { WebSocket } from 'ws';
17
15
  import * as HqServerAuth from './auth.js';
@@ -41,6 +39,7 @@ export declare const agentMessageToEntry: typeof HqServerUtils.agentMessageToEnt
41
39
  export declare const readLocalSubagentTranscript: typeof HqServerUtils.readLocalSubagentTranscript;
42
40
  export declare const lanIPv4Addresses: typeof HqServerUtils.lanIPv4Addresses;
43
41
  export declare const hqRuntimeMarkerPath: typeof HqServerUtils.hqRuntimeMarkerPath;
42
+ import type { HqIpAllowlist } from './ip-allowlist.js';
44
43
  import type { LoginAttemptStore } from './login-attempt-store.js';
45
44
  import type { ConnectedClient, HqRouterMutableAuth, HqSessionEntry, HqSnapshotBroadcaster, ProjectDetail, TranscriptRing } from './types.js';
46
45
  export type { ConnectedClient, HqSnapshotBroadcaster, ProjectDetail, TranscriptRing };
@@ -99,6 +98,8 @@ export interface HqRouterDeps {
99
98
  * Defaults to 0, which ignores `X-Forwarded-For` entirely.
100
99
  */
101
100
  trustedProxyHops?: number | undefined;
101
+ /** TCP-peer allowlist shared by HTTP and WebSocket admission. */
102
+ ipAllowlist?: HqIpAllowlist | undefined;
102
103
  }
103
104
  /**
104
105
  * Create the HTTP request listener that handles all `/api/*` routes as well
@@ -0,0 +1,7 @@
1
+ export interface ResolveHqPasswordOptions {
2
+ explicitPassword?: string | undefined;
3
+ environment?: NodeJS.ProcessEnv | undefined;
4
+ }
5
+ /** Resolve HQ password input without putting Docker/Kubernetes secrets in argv or env values. */
6
+ export declare function resolveHqPasswordInput(options?: ResolveHqPasswordOptions): Promise<string | undefined>;
7
+ //# sourceMappingURL=secret-input.d.ts.map
@@ -107,7 +107,8 @@ export interface HqRouterMutableAuth {
107
107
  /**
108
108
  * Session record stored in the server-side `sessions` Map.
109
109
  *
110
- * - `kind: 'password'` — created by `/api/login`; full browser access.
110
+ * - `kind: 'password'` — created by `/api/login`; full browser access unless
111
+ * the dedicated mobile endpoint stamped a least-privilege capability set.
111
112
  * - `kind: 'token'` — created by `/api/auth/bootstrap`; scoped to the
112
113
  * originating browser token's ID and capabilities so revocation/expiry
113
114
  * of that token invalidates the session too.
@@ -117,7 +118,7 @@ export interface HqSessionEntry {
117
118
  readonly kind: 'password' | 'token';
118
119
  /** For token sessions: the browser-token ID this session was minted from. */
119
120
  readonly tokenId?: string | undefined;
120
- /** For token sessions: capabilities inherited from the source token. */
121
+ /** Token capabilities, or a server-stamped scope for a mobile password session. */
121
122
  readonly capabilities?: string[] | undefined;
122
123
  /**
123
124
  * Set when a password login passed but 2FA verification is still pending.
@@ -8,6 +8,7 @@ import type { Duplex } from 'node:stream';
8
8
  import { type HqToken } from '@wrongstack/core/hq';
9
9
  import type { WebSocket, WebSocketServer } from 'ws';
10
10
  import type { HqAuthState } from './auth-state.js';
11
+ import type { HqIpAllowlist } from './ip-allowlist.js';
11
12
  import type { ConnectedClient, HqSessionEntry, TranscriptRing } from './types.js';
12
13
  interface HqUpgradeHandlerDeps {
13
14
  host: string;
@@ -28,6 +29,7 @@ interface HqUpgradeHandlerDeps {
28
29
  auditLog: import('@wrongstack/core/hq').HqCommandAuditLog;
29
30
  snapshotBroadcaster: ReturnType<typeof import('./snapshot.js').createSnapshotBroadcaster>;
30
31
  wss: WebSocketServer;
32
+ ipAllowlist?: HqIpAllowlist | undefined;
31
33
  }
32
34
  export declare function handleHqUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer, deps: HqUpgradeHandlerDeps): void;
33
35
  export declare function handleHqConnection(ws: WebSocket, req: IncomingMessage, pathname: string, deps: HqUpgradeHandlerDeps): void;
@@ -95,6 +95,12 @@ export declare function buildHttpUrl(host: string, port: number, token?: string)
95
95
  */
96
96
  export declare function buildBootstrapHttpUrl(host: string, port: number, bootstrapCode: string): string;
97
97
  export declare function buildClientWsUrl(host: string, port: number, token?: string): string;
98
+ /**
99
+ * Validate and normalize a browser-facing HQ origin supplied for a persistent
100
+ * HTTPS reverse proxy. Paths, credentials, query strings and fragments are
101
+ * refused because the value becomes a Host/Origin trust-boundary entry.
102
+ */
103
+ export declare function normalizeHqPublicOrigin(value: string): string;
98
104
  export declare function hqRuntimeMarkerPath(dataDir: string): string;
99
105
  /** Non-internal IPv4 addresses, so we can print URLs reachable from other machines. */
100
106
  export declare function lanIPv4Addresses(): string[];
@@ -10,9 +10,10 @@ import {
10
10
  readLocalSubagentTranscript,
11
11
  sanitizeApiError,
12
12
  startHqServer
13
- } from "./chunk-C2E24IXZ.js";
13
+ } from "./chunk-3I5ZJXM5.js";
14
14
  import "./chunk-HXJYHAR4.js";
15
- import "./chunk-Q5GTM25S.js";
15
+ import "./chunk-I7V2OYVH.js";
16
+ import "./chunk-Q7E3BPDU.js";
16
17
  export {
17
18
  DEFAULT_HOST,
18
19
  DEFAULT_PORT,
@@ -26,4 +27,4 @@ export {
26
27
  sanitizeApiError,
27
28
  startHqServer
28
29
  };
29
- //# sourceMappingURL=hq-server-OILU3NPI.js.map
30
+ //# sourceMappingURL=hq-server-CNUTBWTK.js.map
@@ -30,12 +30,16 @@ interface HqServerOptions {
30
30
  clientCleanupIntervalMs?: number;
31
31
  sessionSnapshotTtlMs?: number;
32
32
  password?: string;
33
+ /** Keep an existing Settings-managed password instead of reapplying the startup secret. */
34
+ bootstrapPasswordOnly?: boolean;
33
35
  allowInsecureOpen?: boolean;
34
36
  secureCookies?: boolean;
35
37
  requireBrowserAuth?: boolean;
36
38
  allowFileOrigin?: boolean;
37
39
  tokenTtlMs?: number;
38
40
  trustedProxyHops?: number;
41
+ /** Optional exact-IP/CIDR admission list. Empty/absent means unrestricted. */
42
+ ipAllowlist?: readonly string[];
39
43
  }
40
44
  import type { HqServerHandle } from './hq-server/handle-types.js';
41
45
  export type { HqFirstRunSetup, HqServerHandle, HqStartupConnectionInfo, } from './hq-server/handle-types.js';
@@ -0,0 +1,507 @@
1
+ import {
2
+ resolveHqPasswordInput
3
+ } from "./chunk-AUQDGU5D.js";
4
+ import {
5
+ detectUpdatePackageManager,
6
+ detectUpdatePackageName
7
+ } from "./chunk-DE7BZDNK.js";
8
+ import "./chunk-U3SS4NRH.js";
9
+ import "./chunk-YLQEGYV4.js";
10
+
11
+ // src/hq-service.ts
12
+ import { spawn } from "node:child_process";
13
+ import { constants as fsConstants } from "node:fs";
14
+ import * as fs from "node:fs/promises";
15
+ import * as path from "node:path";
16
+ import { buildChildEnv } from "@wrongstack/core/utils";
17
+ var SERVICE_NAME = "wrongstack-hq.service";
18
+ var UPDATE_SERVICE_NAME = "wrongstack-hq-update.service";
19
+ var UPDATE_TIMER_NAME = "wrongstack-hq-update.timer";
20
+ var SYSTEMD_DIR = "/etc/systemd/system";
21
+ var ENV_DIR = "/etc/wrongstack";
22
+ var ENV_FILE = `${ENV_DIR}/hq.env`;
23
+ var UPDATE_SCRIPT = "/usr/local/libexec/wrongstack-hq-update";
24
+ var MANAGED_MARKER = "Managed by WrongStack HQ service installer";
25
+ function unitQuote(value) {
26
+ if (/[\r\n\0]/.test(value)) throw new Error("systemd values cannot contain line breaks or NUL");
27
+ return `"${value.replaceAll("%", "%%").replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
28
+ }
29
+ function envQuote(value) {
30
+ if (/[\r\n\0]/.test(value))
31
+ throw new Error("HQ service secrets cannot contain line breaks or NUL");
32
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
33
+ }
34
+ function shellQuote(value) {
35
+ if (/\0/.test(value)) throw new Error("shell values cannot contain NUL");
36
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
37
+ }
38
+ function renderHqServiceAssets(options) {
39
+ const bindHost = options.publicOrigin ? "127.0.0.1" : "0.0.0.0";
40
+ const execStart = [
41
+ unitQuote(options.wstackBin),
42
+ "hq",
43
+ "serve",
44
+ "--host",
45
+ bindHost,
46
+ "--port",
47
+ String(options.port),
48
+ "--strict-port"
49
+ ].join(" ");
50
+ const service = `# ${MANAGED_MARKER}
51
+ [Unit]
52
+ Description=WrongStack HQ command center
53
+ Documentation=https://github.com/WrongStack/WrongStack
54
+ Wants=network-online.target
55
+ After=network-online.target
56
+ StartLimitIntervalSec=0
57
+
58
+ [Service]
59
+ Type=simple
60
+ DynamicUser=yes
61
+ StateDirectory=wrongstack-hq
62
+ StateDirectoryMode=0700
63
+ WorkingDirectory=/var/lib/wrongstack-hq
64
+ Environment=NODE_ENV=production
65
+ Environment=HOME=/var/lib/wrongstack-hq
66
+ Environment=WRONGSTACK_HOME=/var/lib/wrongstack-hq
67
+ Environment=WRONGSTACK_HQ_DATA_DIR=/var/lib/wrongstack-hq/hq
68
+ Environment=WRONGSTACK_HQ_SUPPRESS_STARTUP_SECRETS=1
69
+ EnvironmentFile=${ENV_FILE}
70
+ ExecStartPre=/usr/bin/test -x ${unitQuote(options.wstackBin)}
71
+ ExecStart=${execStart}
72
+ Restart=always
73
+ RestartSec=5s
74
+ TimeoutStopSec=30s
75
+ KillSignal=SIGTERM
76
+ StandardOutput=journal
77
+ StandardError=journal
78
+ SyslogIdentifier=wrongstack-hq
79
+ UMask=0077
80
+ NoNewPrivileges=yes
81
+ PrivateTmp=yes
82
+ PrivateDevices=yes
83
+ ProtectSystem=strict
84
+ ProtectHome=yes
85
+ ProtectKernelTunables=yes
86
+ ProtectKernelModules=yes
87
+ ProtectControlGroups=yes
88
+ RestrictSUIDSGID=yes
89
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
90
+
91
+ [Install]
92
+ WantedBy=multi-user.target
93
+ `;
94
+ const updateService = `# ${MANAGED_MARKER}
95
+ [Unit]
96
+ Description=Safely update WrongStack HQ
97
+ Documentation=https://github.com/WrongStack/WrongStack
98
+ Wants=network-online.target
99
+ After=network-online.target
100
+
101
+ [Service]
102
+ Type=oneshot
103
+ User=root
104
+ CacheDirectory=wrongstack-hq-update
105
+ Environment=HOME=/var/cache/wrongstack-hq-update
106
+ ExecStart=${UPDATE_SCRIPT}
107
+ Nice=10
108
+ IOSchedulingClass=best-effort
109
+ IOSchedulingPriority=7
110
+ NoNewPrivileges=yes
111
+ PrivateTmp=yes
112
+ PrivateDevices=yes
113
+ ProtectHome=yes
114
+ ProtectKernelTunables=yes
115
+ ProtectKernelModules=yes
116
+ ProtectControlGroups=yes
117
+ RestrictSUIDSGID=yes
118
+ RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
119
+ `;
120
+ const updateTimer = `# ${MANAGED_MARKER}
121
+ [Unit]
122
+ Description=Daily WrongStack HQ self-update check
123
+
124
+ [Timer]
125
+ OnBootSec=15min
126
+ OnCalendar=*-*-* 04:17:00
127
+ RandomizedDelaySec=1h
128
+ Persistent=true
129
+ Unit=${UPDATE_SERVICE_NAME}
130
+
131
+ [Install]
132
+ WantedBy=timers.target
133
+ `;
134
+ const rollback = options.packageManager === "pnpm" ? `${shellQuote(options.packageManager)} add -g --ignore-scripts "${options.packageName}@$PREVIOUS_VERSION"` : `${shellQuote(options.packageManager)} install -g --ignore-scripts "${options.packageName}@$PREVIOUS_VERSION"`;
135
+ const packageManagerEnvironment = options.packageManager === "pnpm" ? `export PNPM_HOME=${shellQuote(path.dirname(options.wstackBin))}
136
+ ` : "";
137
+ const updateScript = `#!/bin/sh
138
+ # ${MANAGED_MARKER}
139
+ set -eu
140
+
141
+ WSTACK_BIN=${shellQuote(options.wstackBin)}
142
+ PACKAGE_MANAGER=${shellQuote(options.packageManager)}
143
+ SERVICE=${shellQuote(SERVICE_NAME)}
144
+ export PATH=${shellQuote(`${path.dirname(options.wstackBin)}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`)}
145
+ ${packageManagerEnvironment}
146
+
147
+ version_of() {
148
+ "$WSTACK_BIN" --version 2>/dev/null | sed -n 's/^WrongStack \\([^ ]*\\).*/\\1/p' | head -n 1
149
+ }
150
+
151
+ CHECK_OUTPUT=$("$WSTACK_BIN" update --check-only --pm "$PACKAGE_MANAGER") || {
152
+ printf '%s\\n' "$CHECK_OUTPUT" >&2
153
+ exit 1
154
+ }
155
+ printf '%s\\n' "$CHECK_OUTPUT"
156
+ if ! printf '%s\\n' "$CHECK_OUTPUT" | grep -q '^Update available:'; then
157
+ exit 0
158
+ fi
159
+
160
+ PREVIOUS_VERSION=$(version_of)
161
+ if [ -z "$PREVIOUS_VERSION" ] || [ "$PREVIOUS_VERSION" = "dev" ]; then
162
+ echo "Cannot determine an installed release version; refusing unattended update." >&2
163
+ exit 1
164
+ fi
165
+
166
+ SERVICE_STOPPED=0
167
+ ensure_started() {
168
+ if [ "$SERVICE_STOPPED" -eq 1 ]; then
169
+ systemctl start "$SERVICE" || true
170
+ fi
171
+ }
172
+ trap ensure_started EXIT INT TERM
173
+
174
+ systemctl stop "$SERVICE"
175
+ SERVICE_STOPPED=1
176
+ if ! "$WSTACK_BIN" update --pm "$PACKAGE_MANAGER"; then
177
+ echo "Update failed; reinstalling WrongStack $PREVIOUS_VERSION." >&2
178
+ ${rollback}
179
+ exit 1
180
+ fi
181
+
182
+ UPDATED_VERSION=$(version_of)
183
+ if [ -z "$UPDATED_VERSION" ] || [ "$UPDATED_VERSION" = "$PREVIOUS_VERSION" ]; then
184
+ echo "Updated binary did not report a new release; rolling back." >&2
185
+ ${rollback}
186
+ exit 1
187
+ fi
188
+
189
+ systemctl start "$SERVICE"
190
+ SERVICE_STOPPED=0
191
+ sleep 10
192
+ if ! systemctl is-active --quiet "$SERVICE"; then
193
+ echo "Updated HQ did not stay active; rolling back to $PREVIOUS_VERSION." >&2
194
+ systemctl stop "$SERVICE" || true
195
+ SERVICE_STOPPED=1
196
+ ${rollback}
197
+ systemctl start "$SERVICE"
198
+ SERVICE_STOPPED=0
199
+ sleep 5
200
+ systemctl is-active --quiet "$SERVICE"
201
+ exit 1
202
+ fi
203
+
204
+ echo "WrongStack HQ updated: $PREVIOUS_VERSION -> $UPDATED_VERSION"
205
+ `;
206
+ const environment = [
207
+ `# ${MANAGED_MARKER}`,
208
+ `WRONGSTACK_HQ_PASSWORD=${envQuote(options.password)}`,
209
+ "WRONGSTACK_HQ_BOOTSTRAP_PASSWORD_ONLY=1",
210
+ ...options.publicOrigin ? [`WRONGSTACK_HQ_PUBLIC_URL=${envQuote(options.publicOrigin)}`] : [],
211
+ ...options.ipAllowlist ? [`WRONGSTACK_HQ_ALLOWLIST=${envQuote(options.ipAllowlist)}`] : []
212
+ ].join("\n");
213
+ return { service, updateService, updateTimer, updateScript, environment: `${environment}
214
+ ` };
215
+ }
216
+ async function runCommand(command, args) {
217
+ return await new Promise((resolve2, reject) => {
218
+ const child = spawn(command, args, {
219
+ stdio: "pipe",
220
+ windowsHide: true,
221
+ env: buildChildEnv()
222
+ });
223
+ let stdout = "";
224
+ let stderr = "";
225
+ child.stdout?.on("data", (chunk) => {
226
+ stdout += String(chunk);
227
+ });
228
+ child.stderr?.on("data", (chunk) => {
229
+ stderr += String(chunk);
230
+ });
231
+ child.on("error", reject);
232
+ child.on("close", (code) => resolve2({ code, stdout, stderr }));
233
+ });
234
+ }
235
+ async function systemctl(args, allowFailure = false) {
236
+ const result = await runCommand("systemctl", args);
237
+ if (!allowFailure && result.code !== 0) {
238
+ throw new Error(result.stderr.trim() || `systemctl ${args.join(" ")} failed`);
239
+ }
240
+ return result;
241
+ }
242
+ async function writeManagedFile(file, content, mode) {
243
+ const temporary = `${file}.tmp-${process.pid}`;
244
+ await fs.writeFile(temporary, content, { encoding: "utf8", mode });
245
+ await fs.chmod(temporary, mode);
246
+ await fs.rename(temporary, file);
247
+ }
248
+ async function hasForeignManagedTarget(file) {
249
+ try {
250
+ const content = await fs.readFile(file, "utf8");
251
+ return !content.includes(MANAGED_MARKER);
252
+ } catch (cause) {
253
+ return cause.code !== "ENOENT";
254
+ }
255
+ }
256
+ async function findExecutable(name) {
257
+ for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
258
+ if (!directory || !path.isAbsolute(directory)) continue;
259
+ const candidate = path.join(directory, name);
260
+ try {
261
+ await fs.access(candidate, fsConstants.X_OK);
262
+ return candidate;
263
+ } catch {
264
+ }
265
+ }
266
+ return void 0;
267
+ }
268
+ async function findSystemExecutable(name) {
269
+ for (const directory of ["/usr/local/bin", "/usr/bin", "/bin"]) {
270
+ const candidate = path.join(directory, name);
271
+ try {
272
+ await fs.access(candidate, fsConstants.X_OK);
273
+ return candidate;
274
+ } catch {
275
+ }
276
+ }
277
+ return void 0;
278
+ }
279
+ function requireLinuxRoot(deps) {
280
+ if (process.platform !== "linux") {
281
+ deps.renderer.writeError("HQ system service management is supported only on Linux/systemd.\n");
282
+ return false;
283
+ }
284
+ if (process.getuid?.() !== 0) {
285
+ deps.renderer.writeError("Run this command as root (for example with sudo).\n");
286
+ return false;
287
+ }
288
+ return true;
289
+ }
290
+ function serviceHelp(deps) {
291
+ deps.renderer.write(
292
+ "Usage:\n sudo -E wstack hq service install [--port 3499] [--pm npm|pnpm] [--no-auto-update]\n wstack hq service status\n sudo wstack hq service update\n sudo wstack hq service uninstall\n\nInstall requires WRONGSTACK_HQ_PASSWORD (minimum 8 characters).\nOptions: --port <n>, --hq-allowlist <ip,cidr,...>, --pm npm|pnpm, --wstack-bin <path>, --no-auto-update, --force.\n"
293
+ );
294
+ }
295
+ async function installService(deps) {
296
+ if (!requireLinuxRoot(deps)) return 1;
297
+ let password;
298
+ try {
299
+ password = await resolveHqPasswordInput();
300
+ } catch (cause) {
301
+ deps.renderer.writeError(`${cause instanceof Error ? cause.message : String(cause)}
302
+ `);
303
+ return 1;
304
+ }
305
+ if (!password || password.length < 8) {
306
+ deps.renderer.writeError(
307
+ "Set WRONGSTACK_HQ_PASSWORD to at least 8 characters; it will be stored root-only.\n"
308
+ );
309
+ return 1;
310
+ }
311
+ const portValue = deps.flags?.["port"];
312
+ const port = typeof portValue === "string" ? Number(portValue) : 3499;
313
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
314
+ deps.renderer.writeError("--port must be an integer between 1 and 65535.\n");
315
+ return 1;
316
+ }
317
+ const requestedManager = deps.flags?.["pm"] ?? deps.flags?.["package-manager"];
318
+ const detectedManager = typeof requestedManager === "string" ? requestedManager : detectUpdatePackageManager();
319
+ if (detectedManager !== "npm" && detectedManager !== "pnpm") {
320
+ deps.renderer.writeError("HQ unattended updates support --pm npm or --pm pnpm.\n");
321
+ return 1;
322
+ }
323
+ if (!await findSystemExecutable(detectedManager)) {
324
+ deps.renderer.writeError(
325
+ `Could not find system-wide ${detectedManager}; unattended root updates cannot use a home-directory shim.
326
+ `
327
+ );
328
+ return 1;
329
+ }
330
+ const explicitBin = deps.flags?.["wstack-bin"];
331
+ const wstackBin = typeof explicitBin === "string" ? path.resolve(explicitBin) : await findExecutable("wstack");
332
+ if (!wstackBin) {
333
+ deps.renderer.writeError("Could not find a global wstack executable in PATH.\n");
334
+ return 1;
335
+ }
336
+ const resolvedWstackBin = await fs.realpath(wstackBin).catch(() => wstackBin);
337
+ if (resolvedWstackBin === "/root" || resolvedWstackBin.startsWith("/root/") || resolvedWstackBin.startsWith("/home/")) {
338
+ deps.renderer.writeError(
339
+ "The HQ system service requires a system-wide wstack executable outside /root and /home. Install with `sudo npm install -g wrongstack` and retry.\n"
340
+ );
341
+ return 1;
342
+ }
343
+ const publicOriginValue = typeof deps.flags?.["hq-public-url"] === "string" ? deps.flags["hq-public-url"] : process.env.WRONGSTACK_HQ_PUBLIC_URL;
344
+ let publicOrigin;
345
+ if (publicOriginValue) {
346
+ try {
347
+ const parsed = new URL(publicOriginValue);
348
+ if (parsed.protocol !== "https:" || parsed.origin !== publicOriginValue.replace(/\/$/, "")) {
349
+ throw new Error("not an exact HTTPS origin");
350
+ }
351
+ publicOrigin = parsed.origin;
352
+ } catch {
353
+ deps.renderer.writeError("--hq-public-url must be an exact HTTPS origin.\n");
354
+ return 1;
355
+ }
356
+ }
357
+ const allowlistValue = typeof deps.flags?.["hq-allowlist"] === "string" ? deps.flags["hq-allowlist"] : process.env.WRONGSTACK_HQ_ALLOWLIST;
358
+ let ipAllowlist;
359
+ if (allowlistValue !== void 0 && allowlistValue.trim() !== "") {
360
+ try {
361
+ const { parseHqIpAllowlist } = await import("./ip-allowlist-NHXO6AKJ.js");
362
+ ipAllowlist = parseHqIpAllowlist(allowlistValue)?.join(",");
363
+ } catch (cause) {
364
+ deps.renderer.writeError(`${cause instanceof Error ? cause.message : String(cause)}
365
+ `);
366
+ return 1;
367
+ }
368
+ }
369
+ const assets = renderHqServiceAssets({
370
+ wstackBin,
371
+ packageManager: detectedManager,
372
+ packageName: detectUpdatePackageName(),
373
+ password,
374
+ port,
375
+ ...publicOrigin ? { publicOrigin } : {},
376
+ ...ipAllowlist ? { ipAllowlist } : {}
377
+ });
378
+ try {
379
+ await fs.access("/run/systemd/system");
380
+ const systemd = await systemctl(["--version"], true);
381
+ if (systemd.code !== 0) throw new Error("systemctl is not operational");
382
+ } catch {
383
+ deps.renderer.writeError("This host is not running systemd; no service files were written.\n");
384
+ return 1;
385
+ }
386
+ const managedTargets = [
387
+ `${SYSTEMD_DIR}/${SERVICE_NAME}`,
388
+ `${SYSTEMD_DIR}/${UPDATE_SERVICE_NAME}`,
389
+ `${SYSTEMD_DIR}/${UPDATE_TIMER_NAME}`,
390
+ UPDATE_SCRIPT,
391
+ ENV_FILE
392
+ ];
393
+ if (deps.flags?.["force"] !== true) {
394
+ const foreign = [];
395
+ for (const file of managedTargets) {
396
+ if (await hasForeignManagedTarget(file)) foreign.push(file);
397
+ }
398
+ if (foreign.length > 0) {
399
+ deps.renderer.writeError(
400
+ `Refusing to overwrite files not managed by WrongStack:
401
+ ${foreign.map((file) => ` ${file}`).join("\n")}
402
+ Use --force only after reviewing them.
403
+ `
404
+ );
405
+ return 1;
406
+ }
407
+ }
408
+ await fs.mkdir(ENV_DIR, { recursive: true, mode: 448 });
409
+ await fs.mkdir(path.dirname(UPDATE_SCRIPT), { recursive: true, mode: 493 });
410
+ await writeManagedFile(ENV_FILE, assets.environment, 384);
411
+ await writeManagedFile(`${SYSTEMD_DIR}/${SERVICE_NAME}`, assets.service, 420);
412
+ await writeManagedFile(`${SYSTEMD_DIR}/${UPDATE_SERVICE_NAME}`, assets.updateService, 420);
413
+ await writeManagedFile(`${SYSTEMD_DIR}/${UPDATE_TIMER_NAME}`, assets.updateTimer, 420);
414
+ await writeManagedFile(UPDATE_SCRIPT, assets.updateScript, 493);
415
+ await systemctl(["daemon-reload"]);
416
+ await systemctl(["enable", SERVICE_NAME]);
417
+ await systemctl(["restart", SERVICE_NAME]);
418
+ if (deps.flags?.["no-auto-update"] === true) {
419
+ await systemctl(["disable", "--now", UPDATE_TIMER_NAME], true);
420
+ } else {
421
+ await systemctl(["enable", "--now", UPDATE_TIMER_NAME]);
422
+ }
423
+ deps.renderer.write(
424
+ `Installed ${SERVICE_NAME}; listening on ${publicOrigin ? "127.0.0.1" : "0.0.0.0"}:${port}, boot persistence and automatic restart are active.
425
+ ` + (ipAllowlist ? `Network allowlist is active (${ipAllowlist.split(",").length} configured rules + loopback).
426
+ ` : "Network allowlist is disabled; password authentication remains active.\n") + (deps.flags?.["no-auto-update"] === true ? "Automatic updates are disabled.\n" : `Automatic updates are scheduled by ${UPDATE_TIMER_NAME}.
427
+ `)
428
+ );
429
+ return 0;
430
+ }
431
+ async function uninstallService(deps) {
432
+ if (!requireLinuxRoot(deps)) return 1;
433
+ const targets = [
434
+ `${SYSTEMD_DIR}/${SERVICE_NAME}`,
435
+ `${SYSTEMD_DIR}/${UPDATE_SERVICE_NAME}`,
436
+ `${SYSTEMD_DIR}/${UPDATE_TIMER_NAME}`,
437
+ UPDATE_SCRIPT
438
+ ];
439
+ const timerIsForeign = await hasForeignManagedTarget(`${SYSTEMD_DIR}/${UPDATE_TIMER_NAME}`);
440
+ const serviceIsForeign = await hasForeignManagedTarget(`${SYSTEMD_DIR}/${SERVICE_NAME}`);
441
+ if (!timerIsForeign) await systemctl(["disable", "--now", UPDATE_TIMER_NAME], true);
442
+ if (!serviceIsForeign) await systemctl(["disable", "--now", SERVICE_NAME], true);
443
+ const preserved = [];
444
+ for (const file of targets) {
445
+ if (await hasForeignManagedTarget(file)) {
446
+ preserved.push(file);
447
+ continue;
448
+ }
449
+ await fs.rm(file, { force: true });
450
+ }
451
+ await systemctl(["daemon-reload"]);
452
+ deps.renderer.write(
453
+ `Removed WrongStack-managed HQ systemd files. Data and ${ENV_FILE} were preserved.
454
+ `
455
+ );
456
+ if (preserved.length > 0) {
457
+ deps.renderer.write(
458
+ `Preserved files not managed by WrongStack:
459
+ ${preserved.map((file) => ` ${file}`).join("\n")}
460
+ `
461
+ );
462
+ }
463
+ return 0;
464
+ }
465
+ var hqServiceCmd = async (args, deps) => {
466
+ const action = args[0];
467
+ if (deps.flags?.["help"] === true || action === "help" || !action) {
468
+ serviceHelp(deps);
469
+ return 0;
470
+ }
471
+ try {
472
+ if (action === "install") return await installService(deps);
473
+ if (action === "uninstall") return await uninstallService(deps);
474
+ if (action === "update") {
475
+ if (!requireLinuxRoot(deps)) return 1;
476
+ const result = await systemctl(["start", UPDATE_SERVICE_NAME]);
477
+ if (result.stdout) deps.renderer.write(result.stdout);
478
+ deps.renderer.write("HQ update check completed.\n");
479
+ return 0;
480
+ }
481
+ if (action === "status") {
482
+ if (process.platform !== "linux") {
483
+ deps.renderer.writeError("HQ system service status is supported only on Linux/systemd.\n");
484
+ return 1;
485
+ }
486
+ const result = await systemctl(
487
+ ["status", SERVICE_NAME, UPDATE_TIMER_NAME, "--no-pager"],
488
+ true
489
+ );
490
+ deps.renderer.write(result.stdout || result.stderr);
491
+ return result.code ?? 1;
492
+ }
493
+ } catch (cause) {
494
+ deps.renderer.writeError(`${cause instanceof Error ? cause.message : String(cause)}
495
+ `);
496
+ return 1;
497
+ }
498
+ deps.renderer.writeError(`Unknown HQ service action: ${action}
499
+ `);
500
+ serviceHelp(deps);
501
+ return 1;
502
+ };
503
+ export {
504
+ hqServiceCmd,
505
+ renderHqServiceAssets
506
+ };
507
+ //# sourceMappingURL=hq-service-L5INQFM4.js.map
@@ -0,0 +1,22 @@
1
+ import type { SubcommandHandler } from './subcommands/contracts.js';
2
+ type ServerPackageManager = 'npm' | 'pnpm';
3
+ export interface HqServiceAssets {
4
+ service: string;
5
+ updateService: string;
6
+ updateTimer: string;
7
+ updateScript: string;
8
+ environment: string;
9
+ }
10
+ export interface HqServiceAssetOptions {
11
+ wstackBin: string;
12
+ packageManager: ServerPackageManager;
13
+ packageName: 'wrongstack' | '@wrongstack/cli';
14
+ password: string;
15
+ port: number;
16
+ publicOrigin?: string | undefined;
17
+ ipAllowlist?: string | undefined;
18
+ }
19
+ export declare function renderHqServiceAssets(options: HqServiceAssetOptions): HqServiceAssets;
20
+ export declare const hqServiceCmd: SubcommandHandler;
21
+ export {};
22
+ //# sourceMappingURL=hq-service.d.ts.map