@indigoai-us/hq-cli 5.192.2 → 5.193.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.193.0] — 2026-09-25
6
+
7
+ ### Fixed
8
+
9
+ - `hq secrets set` now handles a closed stdin descriptor during terminal cleanup.
10
+
11
+ ### Improved
12
+
13
+ - Sentry reports for failed skill-wrapper replacement now include bounded ownership-state tags.
14
+
5
15
  ## [5.192.2] — 2026-09-25
6
16
 
7
17
  ### Fixed
@@ -188,12 +188,23 @@ function promptSecretInteractively() {
188
188
  let value = "";
189
189
  process.stdin.setEncoding("utf8");
190
190
  const cleanup = () => {
191
- if (process.stdin.isTTY) {
192
- process.stdin.setRawMode(false);
191
+ try {
192
+ if (process.stdin.isTTY) {
193
+ try {
194
+ process.stdin.setRawMode(false);
195
+ }
196
+ catch (error) {
197
+ if (error?.code !== "EBADF") {
198
+ throw error;
199
+ }
200
+ }
201
+ }
202
+ }
203
+ finally {
204
+ process.stdin.removeListener("data", onData);
205
+ process.stdin.removeListener("end", onEnd);
206
+ process.stdin.pause();
193
207
  }
194
- process.stdin.removeListener("data", onData);
195
- process.stdin.removeListener("end", onEnd);
196
- process.stdin.pause();
197
208
  };
198
209
  const onEnd = () => {
199
210
  cleanup();
@@ -1,4 +1,13 @@
1
1
  import * as fs from "node:fs";
2
+ export interface CompanySkillWrapperOwnershipDiagnostics {
3
+ kind: "directory" | "symlink" | "file" | "other";
4
+ bridgeState: "all_live" | "partial" | "none" | "empty" | "not_applicable";
5
+ markerState: "bound" | "missing" | "invalid" | "unbound" | "unavailable" | "not_applicable";
6
+ linkTarget: "company_skills" | "outside_company_skills" | "unavailable" | "not_applicable";
7
+ platform: "win32" | "darwin" | "linux" | "other";
8
+ nameEncoding: "percent_encoded" | "literal";
9
+ }
10
+ export declare function companySkillWrapperSentryTagsFor(error: unknown): Record<string, string> | undefined;
2
11
  export interface SurfaceCompanySkillInput {
3
12
  hqRoot: string;
4
13
  companySlug: string;
@@ -62,20 +62,6 @@ function symlinkTarget(linkPath) {
62
62
  function generatedMarkerPath(wrapperPath) {
63
63
  return path.join(path.dirname(wrapperPath), WRAPPER_MARKER_DIR, `${path.basename(wrapperPath)}.json`);
64
64
  }
65
- function hasBoundGeneratedMarker(wrapperPath, companySlug) {
66
- try {
67
- const marker = JSON.parse(fs.readFileSync(generatedMarkerPath(wrapperPath), "utf8"));
68
- const wrapper = fs.lstatSync(wrapperPath);
69
- return (marker.version === 2 &&
70
- marker.companySlug === companySlug &&
71
- marker.wrapperDev === wrapper.dev &&
72
- marker.wrapperIno === wrapper.ino &&
73
- marker.wrapperBirthtimeMs === wrapper.birthtimeMs);
74
- }
75
- catch {
76
- return false;
77
- }
78
- }
79
65
  function writeGeneratedMarker(wrapperPath, companySlug, skillSlug) {
80
66
  const markerPath = generatedMarkerPath(wrapperPath);
81
67
  try {
@@ -104,21 +90,116 @@ function removeGeneratedMarker(wrapperPath) {
104
90
  // never sufficient to surface content on its own.
105
91
  }
106
92
  }
107
- function isGeneratedNamespacedWrapper(wrapperPath, hqRoot, companySlug) {
93
+ const COMPANY_SKILL_WRAPPER_DIAGNOSTICS = Symbol("company-skill-wrapper-diagnostics");
94
+ function companySkillWrapperOwnershipError(wrapperPath, diagnostics) {
95
+ const error = new Error(`Cannot replace '${wrapperPath}' because it is not an HQ-generated skill wrapper.`);
96
+ Object.defineProperty(error, COMPANY_SKILL_WRAPPER_DIAGNOSTICS, {
97
+ value: diagnostics,
98
+ });
99
+ return error;
100
+ }
101
+ export function companySkillWrapperSentryTagsFor(error) {
102
+ if (!(error instanceof Error))
103
+ return undefined;
104
+ const diagnostics = error[COMPANY_SKILL_WRAPPER_DIAGNOSTICS];
105
+ if (!diagnostics)
106
+ return undefined;
107
+ return {
108
+ hq_cli_skill_wrapper_kind: diagnostics.kind,
109
+ hq_cli_skill_wrapper_bridge_state: diagnostics.bridgeState,
110
+ hq_cli_skill_wrapper_marker_state: diagnostics.markerState,
111
+ hq_cli_skill_wrapper_link_target: diagnostics.linkTarget,
112
+ hq_cli_skill_wrapper_platform: diagnostics.platform,
113
+ hq_cli_skill_wrapper_name_encoding: diagnostics.nameEncoding,
114
+ };
115
+ }
116
+ function inspectGeneratedNamespacedWrapper(wrapperPath, hqRoot, companySlug) {
108
117
  const companySkillsRoot = path.join(hqRoot, "companies", companySlug, "skills");
109
- const wrapperTarget = symlinkTarget(wrapperPath);
110
- if (wrapperTarget)
111
- return isWithin(companySkillsRoot, wrapperTarget);
112
- const wrapper = lstat(wrapperPath);
113
- if (!wrapper?.isDirectory())
114
- return false;
118
+ const wrapperEntry = lstat(wrapperPath);
119
+ if (wrapperEntry?.isSymbolicLink()) {
120
+ const target = symlinkTarget(wrapperPath);
121
+ const linkTarget = target === undefined
122
+ ? "unavailable"
123
+ : isWithin(companySkillsRoot, target)
124
+ ? "company_skills"
125
+ : "outside_company_skills";
126
+ return {
127
+ owned: linkTarget === "company_skills",
128
+ diagnostics: {
129
+ kind: "symlink",
130
+ bridgeState: "not_applicable",
131
+ markerState: "not_applicable",
132
+ linkTarget,
133
+ },
134
+ };
135
+ }
136
+ if (!wrapperEntry?.isDirectory()) {
137
+ return {
138
+ owned: false,
139
+ diagnostics: {
140
+ kind: wrapperEntry?.isFile() ? "file" : "other",
141
+ bridgeState: "not_applicable",
142
+ markerState: "not_applicable",
143
+ linkTarget: "not_applicable",
144
+ },
145
+ };
146
+ }
115
147
  const entries = fs.readdirSync(wrapperPath).filter((entry) => !entry.startsWith("."));
116
- const containsOnlyLiveBridges = (entries.length > 0 &&
117
- entries.every((entry) => {
118
- const target = symlinkTarget(path.join(wrapperPath, entry));
119
- return target !== undefined && isWithin(companySkillsRoot, target);
120
- }));
121
- return containsOnlyLiveBridges || hasBoundGeneratedMarker(wrapperPath, companySlug);
148
+ const liveBridgeCount = entries.reduce((count, entry) => {
149
+ const target = symlinkTarget(path.join(wrapperPath, entry));
150
+ return count + (target !== undefined && isWithin(companySkillsRoot, target) ? 1 : 0);
151
+ }, 0);
152
+ const bridgeState = entries.length === 0
153
+ ? "empty"
154
+ : liveBridgeCount === entries.length
155
+ ? "all_live"
156
+ : liveBridgeCount > 0
157
+ ? "partial"
158
+ : "none";
159
+ const markerState = generatedMarkerState(wrapperPath, companySlug);
160
+ return {
161
+ owned: bridgeState === "all_live" || markerState === "bound",
162
+ diagnostics: {
163
+ kind: "directory",
164
+ bridgeState,
165
+ markerState,
166
+ linkTarget: "not_applicable",
167
+ },
168
+ };
169
+ }
170
+ function isGeneratedNamespacedWrapper(wrapperPath, hqRoot, companySlug) {
171
+ return inspectGeneratedNamespacedWrapper(wrapperPath, hqRoot, companySlug).owned;
172
+ }
173
+ function generatedMarkerState(wrapperPath, companySlug) {
174
+ let markerContents;
175
+ try {
176
+ markerContents = fs.readFileSync(generatedMarkerPath(wrapperPath), "utf8");
177
+ }
178
+ catch (error) {
179
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"
180
+ ? "missing"
181
+ : "unavailable";
182
+ }
183
+ let marker;
184
+ try {
185
+ marker = JSON.parse(markerContents);
186
+ }
187
+ catch {
188
+ return "invalid";
189
+ }
190
+ try {
191
+ const wrapper = fs.lstatSync(wrapperPath);
192
+ return marker.version === 2 &&
193
+ marker.companySlug === companySlug &&
194
+ marker.wrapperDev === wrapper.dev &&
195
+ marker.wrapperIno === wrapper.ino &&
196
+ marker.wrapperBirthtimeMs === wrapper.birthtimeMs
197
+ ? "bound"
198
+ : "unbound";
199
+ }
200
+ catch {
201
+ return "unavailable";
202
+ }
122
203
  }
123
204
  function isGeneratedLegacyEntry(legacyPath, sourceDir, skillFile) {
124
205
  const target = symlinkTarget(legacyPath);
@@ -224,8 +305,19 @@ export function surfaceCompanySkill(input) {
224
305
  const backupPath = `${stagingPath}.previous`;
225
306
  let previousMoved = false;
226
307
  if (currentWrapper) {
227
- if (!isGeneratedNamespacedWrapper(wrapperPath, input.hqRoot, input.companySlug)) {
228
- throw new Error(`Cannot replace '${wrapperPath}' because it is not an HQ-generated skill wrapper.`);
308
+ const ownership = inspectGeneratedNamespacedWrapper(wrapperPath, input.hqRoot, input.companySlug);
309
+ if (!ownership.owned) {
310
+ const runtimePlatform = process.platform;
311
+ const platform = win32
312
+ ? "win32"
313
+ : runtimePlatform === "darwin" || runtimePlatform === "linux"
314
+ ? runtimePlatform
315
+ : "other";
316
+ throw companySkillWrapperOwnershipError(wrapperPath, {
317
+ ...ownership.diagnostics,
318
+ platform,
319
+ nameEncoding: win32 ? "percent_encoded" : "literal",
320
+ });
229
321
  }
230
322
  renameEntry(wrapperPath, backupPath);
231
323
  previousMoved = true;
package/dist/sentry.js CHANGED
@@ -5,6 +5,7 @@ import { beforeBreadcrumb } from "./utils/breadcrumb-buffer.js";
5
5
  import { CLI_VERSION } from "./cli-version.js";
6
6
  import { getCachedSentryUser } from "./utils/sentry-identity.js";
7
7
  import { isEpipe } from "./utils/epipe.js";
8
+ import { companySkillWrapperSentryTagsFor } from "./lib/company-skill-wrapper.js";
8
9
  import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
9
10
  import { incompleteInstallCaptureContext, incompleteInstallMessage, } from "./utils/incomplete-install-error.js";
10
11
  import { installTreeTornSentryTagsFor, sentryFingerprintFor, sentryMcpCollisionContextFor, } from "./utils/sentry-fingerprint.js";
@@ -84,6 +85,10 @@ export function epipeAwareBeforeSend(event, hint) {
84
85
  if (fingerprint)
85
86
  event.fingerprint = fingerprint;
86
87
  }
88
+ const skillWrapperTags = companySkillWrapperSentryTagsFor(hint?.originalException);
89
+ if (skillWrapperTags) {
90
+ event.tags = { ...event.tags, ...skillWrapperTags };
91
+ }
87
92
  const installTreeTags = installTreeTornSentryTagsFor(hint?.originalException);
88
93
  if (installTreeTags) {
89
94
  event.tags = { ...event.tags, ...installTreeTags };
@@ -99,5 +99,5 @@ export declare const CONTRIBUTION_KEYS: ContributionKey[];
99
99
  */
100
100
  export declare function payloadFor(key: ContributionKey, item: string): string;
101
101
  /** Keys whose contributions are wired by a host symlink (skip `merge` rows). */
102
- export declare const SYMLINK_KEYS: ("workers" | "mcp" | "knowledge" | "skills" | "commands" | "hooks" | "policies" | "scripts")[];
102
+ export declare const SYMLINK_KEYS: ("workers" | "mcp" | "skills" | "knowledge" | "commands" | "hooks" | "policies" | "scripts")[];
103
103
  //# sourceMappingURL=contribution-table.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.192.2",
3
+ "version": "5.193.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {