@indigoai-us/hq-cli 5.119.12 → 5.119.15
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 +26 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +36 -3
- package/dist/command-catalog.generated.d.ts +12 -0
- package/dist/command-catalog.generated.js +15 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/core.js +13 -1
- package/dist/commands/version.d.ts +10 -0
- package/dist/commands/version.js +11 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +4 -6
- package/dist/lib/index-render/company-knowledge.js +80 -17
- package/dist/lib/index-render/shared.d.ts +12 -0
- package/dist/lib/index-render/shared.js +37 -0
- package/dist/utils/people.d.ts +4 -11
- package/dist/utils/people.js +5 -20
- package/dist/utils/version-gate.d.ts +8 -0
- package/dist/utils/version-gate.js +29 -3
- package/dist/utils/version-report.d.ts +27 -0
- package/dist/utils/version-report.js +97 -0
- package/dist/version-request.d.ts +7 -0
- package/dist/version-request.js +22 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.119.15] — 2026-09-18
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `hq people resolve <name>` now reports ambiguity when the name matches more
|
|
10
|
+
than one person instead of selecting the first match.
|
|
11
|
+
|
|
12
|
+
## [5.119.13] — 2026-09-18
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `hq version` (and `hq --version`) print the installed HQ version, the CLI
|
|
17
|
+
version labelled as CLI, and the latest hq-core release. If a newer HQ is
|
|
18
|
+
out it says to run `/update-hq`; if the lookup cannot reach the network it
|
|
19
|
+
says so instead of going quiet. `hq version --json` prints the same payload.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `hq core rebuild-index company-knowledge` no longer silently replaces a
|
|
24
|
+
hand-written company knowledge INDEX. If the file is not the last generated
|
|
25
|
+
output, the command refuses, prints a short diff, and asks for `--accept`.
|
|
26
|
+
`<!-- hq:keep -->` opts the whole file out; `<!-- hq:keep -->` … `<!-- /hq:keep -->`
|
|
27
|
+
fences are kept while the table regenerates. Subdirectory README headings
|
|
28
|
+
are projected into the description column so the generated index is usable
|
|
29
|
+
without hand edits.
|
|
30
|
+
|
|
5
31
|
## [5.119.12] — 2026-09-18
|
|
6
32
|
|
|
7
33
|
### Fixed
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# - .md files → first `#` heading (stripped)
|
|
9
9
|
# - .yaml files → `description:` field if present
|
|
10
10
|
# - .json files → `description` field if present
|
|
11
|
-
# - directories → "{N} item(s)"
|
|
11
|
+
# - directories → README.md `#` heading plus "{N} item(s)", or "{N} item(s)"
|
|
12
12
|
#
|
|
13
13
|
# Most company knowledge dirs are embedded git repos (160000 gitlinks). The
|
|
14
14
|
# generated INDEX.md lives inside the inner repo; HQ git won't track its
|
|
@@ -42,7 +42,15 @@ describe_item() {
|
|
|
42
42
|
if [[ -d "$path" ]]; then
|
|
43
43
|
local n
|
|
44
44
|
n=$(find "$path" -mindepth 1 -maxdepth 1 ! -name '.*' 2>/dev/null | wc -l | tr -d ' ')
|
|
45
|
-
|
|
45
|
+
local h=""
|
|
46
|
+
if [[ -f "$path/README.md" ]]; then
|
|
47
|
+
h=$(awk '/^# / { sub(/^# +/, ""); print; exit }' "$path/README.md" 2>/dev/null || true)
|
|
48
|
+
fi
|
|
49
|
+
if [[ -n "$h" ]]; then
|
|
50
|
+
echo "${h} (${n} item(s))"
|
|
51
|
+
else
|
|
52
|
+
echo "${n} item(s)"
|
|
53
|
+
fi
|
|
46
54
|
return
|
|
47
55
|
fi
|
|
48
56
|
case "$name" in
|
|
@@ -70,6 +78,26 @@ describe_item() {
|
|
|
70
78
|
esac
|
|
71
79
|
}
|
|
72
80
|
|
|
81
|
+
sha256_file() {
|
|
82
|
+
if command -v sha256sum >/dev/null 2>&1; then
|
|
83
|
+
sha256sum "$1" | awk '{print $1}'
|
|
84
|
+
else
|
|
85
|
+
shasum -a 256 "$1" | awk '{print $1}'
|
|
86
|
+
fi
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
stamp_generated() {
|
|
90
|
+
local tmp="$1"
|
|
91
|
+
local out="$2"
|
|
92
|
+
local norm
|
|
93
|
+
norm=$(mktemp)
|
|
94
|
+
sed 's/^> Auto-generated. Updated: [0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$/> Auto-generated. Updated: <date>/' "$tmp" > "$norm"
|
|
95
|
+
local hash
|
|
96
|
+
hash=$(sha256_file "$norm")
|
|
97
|
+
rm -f "$norm"
|
|
98
|
+
awk -v h="$hash" '{print} /^> Auto-generated. Updated: / {print "<!-- hq-generated-sha256: " h " -->"}' "$tmp" > "$out"
|
|
99
|
+
}
|
|
100
|
+
|
|
73
101
|
write_knowledge_index() {
|
|
74
102
|
local co="$1"
|
|
75
103
|
local kdir="companies/${co}/knowledge"
|
|
@@ -77,6 +105,8 @@ write_knowledge_index() {
|
|
|
77
105
|
local out="${kdir}/INDEX.md"
|
|
78
106
|
local title
|
|
79
107
|
title="$(titleize "$co") Knowledge"
|
|
108
|
+
local tmp
|
|
109
|
+
tmp=$(mktemp)
|
|
80
110
|
|
|
81
111
|
{
|
|
82
112
|
echo "# ${title}"
|
|
@@ -108,7 +138,10 @@ write_knowledge_index() {
|
|
|
108
138
|
find -L "$kdir" -mindepth 1 -maxdepth 1 -type f 2>/dev/null | sort
|
|
109
139
|
}
|
|
110
140
|
)
|
|
111
|
-
} > "$
|
|
141
|
+
} > "$tmp"
|
|
142
|
+
|
|
143
|
+
stamp_generated "$tmp" "$out"
|
|
144
|
+
rm -f "$tmp"
|
|
112
145
|
|
|
113
146
|
echo "rebuild-company-knowledge-index: wrote ${out}" >&2
|
|
114
147
|
}
|
|
@@ -805,6 +805,18 @@ export declare const COMMAND_CATALOG: readonly [{
|
|
|
805
805
|
readonly description: "Also report this company's workspace plan state (Starter lock)";
|
|
806
806
|
}];
|
|
807
807
|
readonly subcommands: readonly [];
|
|
808
|
+
}, {
|
|
809
|
+
readonly name: "version";
|
|
810
|
+
readonly description: "Show HQ, CLI, and latest hq-core versions";
|
|
811
|
+
readonly aliases: readonly [];
|
|
812
|
+
readonly hidden: false;
|
|
813
|
+
readonly usage: "[options]";
|
|
814
|
+
readonly arguments: readonly [];
|
|
815
|
+
readonly options: readonly [{
|
|
816
|
+
readonly flags: "--json";
|
|
817
|
+
readonly description: "Output the same payload as JSON";
|
|
818
|
+
}];
|
|
819
|
+
readonly subcommands: readonly [];
|
|
808
820
|
}, {
|
|
809
821
|
readonly name: "auth";
|
|
810
822
|
readonly description: "Manage the local HQ Cognito session";
|
|
@@ -1029,6 +1029,21 @@ export const COMMAND_CATALOG = [
|
|
|
1029
1029
|
],
|
|
1030
1030
|
"subcommands": []
|
|
1031
1031
|
},
|
|
1032
|
+
{
|
|
1033
|
+
"name": "version",
|
|
1034
|
+
"description": "Show HQ, CLI, and latest hq-core versions",
|
|
1035
|
+
"aliases": [],
|
|
1036
|
+
"hidden": false,
|
|
1037
|
+
"usage": "[options]",
|
|
1038
|
+
"arguments": [],
|
|
1039
|
+
"options": [
|
|
1040
|
+
{
|
|
1041
|
+
"flags": "--json",
|
|
1042
|
+
"description": "Output the same payload as JSON"
|
|
1043
|
+
}
|
|
1044
|
+
],
|
|
1045
|
+
"subcommands": []
|
|
1046
|
+
},
|
|
1032
1047
|
{
|
|
1033
1048
|
"name": "auth",
|
|
1034
1049
|
"description": "Manage the local HQ Cognito session",
|
|
@@ -171,6 +171,12 @@ export declare const REGISTRATION_PLAN: readonly [{
|
|
|
171
171
|
readonly parent: "program";
|
|
172
172
|
readonly module: "./commands/whoami.js";
|
|
173
173
|
readonly exportName: "registerWhoamiCommand";
|
|
174
|
+
}, {
|
|
175
|
+
readonly type: "registrar";
|
|
176
|
+
readonly root: "version";
|
|
177
|
+
readonly parent: "program";
|
|
178
|
+
readonly module: "./commands/version.js";
|
|
179
|
+
readonly exportName: "registerVersionCommand";
|
|
174
180
|
}, {
|
|
175
181
|
readonly type: "registrar";
|
|
176
182
|
readonly root: "auth";
|
|
@@ -31,6 +31,7 @@ export const REGISTRATION_PLAN = [
|
|
|
31
31
|
{ type: "registrar", root: "login", parent: "program", module: "./commands/login.js", exportName: "registerLoginCommand" },
|
|
32
32
|
{ type: "registrar", root: "logout", parent: "program", module: "./commands/logout.js", exportName: "registerLogoutCommand" },
|
|
33
33
|
{ type: "registrar", root: "whoami", parent: "program", module: "./commands/whoami.js", exportName: "registerWhoamiCommand" },
|
|
34
|
+
{ type: "registrar", root: "version", parent: "program", module: "./commands/version.js", exportName: "registerVersionCommand" },
|
|
34
35
|
{ type: "registrar", root: "auth", parent: "program", module: "./commands/auth.js", exportName: "registerAuthCommands" },
|
|
35
36
|
{ type: "registrar", root: "secrets", parent: "program", module: "./commands/secrets.js", exportName: "registerSecretsCommand" },
|
|
36
37
|
{ type: "registrar", root: "db", parent: "program", module: "./commands/db.js", exportName: "registerDbCommand" },
|
package/dist/commands/core.js
CHANGED
|
@@ -408,7 +408,19 @@ export function registerCoreCommands(program) {
|
|
|
408
408
|
const scope = core.opts();
|
|
409
409
|
const hqRoot = resolveLiveRoot({ hqRoot: scope.hqRoot });
|
|
410
410
|
const operands = cmd.args.length > 0 ? cmd.args : args;
|
|
411
|
-
|
|
411
|
+
const passthrough = operands.slice(1);
|
|
412
|
+
const force = passthrough.includes("--accept");
|
|
413
|
+
const rendererArgs = passthrough.filter((arg) => arg !== "--accept");
|
|
414
|
+
const refused = [];
|
|
415
|
+
renderIndexTarget(entry.renderer, {
|
|
416
|
+
root: hqRoot,
|
|
417
|
+
log: (message) => process.stderr.write(`${message}\n`),
|
|
418
|
+
force,
|
|
419
|
+
refused,
|
|
420
|
+
}, rendererArgs);
|
|
421
|
+
if (refused.length > 0) {
|
|
422
|
+
throw expectedUserError(`rebuild-index: refused to overwrite ${refused.length} hand-edited INDEX.md file(s). Re-run with --accept to overwrite.`);
|
|
423
|
+
}
|
|
412
424
|
});
|
|
413
425
|
for (const entry of SCAFFOLD_COMMANDS) {
|
|
414
426
|
core
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hq version — labelled HQ vs CLI vs latest hq-core.
|
|
3
|
+
*
|
|
4
|
+
* `hq --version` is intercepted on the fast path in index.ts and prints the
|
|
5
|
+
* same report. This registrar exists so `hq --help` lists the command and so
|
|
6
|
+
* `hq version --json` still works if the fast path is skipped.
|
|
7
|
+
*/
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
export declare function registerVersionCommand(program: Command): void;
|
|
10
|
+
//# sourceMappingURL=version.d.ts.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { runVersionCommand } from "../utils/version-report.js";
|
|
2
|
+
export function registerVersionCommand(program) {
|
|
3
|
+
program
|
|
4
|
+
.command("version")
|
|
5
|
+
.description("Show HQ, CLI, and latest hq-core versions")
|
|
6
|
+
.option("--json", "Output the same payload as JSON")
|
|
7
|
+
.action(async () => {
|
|
8
|
+
await runVersionCommand(process.argv);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=version.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import "./node-preflight.js";
|
|
3
3
|
import "./node-network-compat.js";
|
|
4
|
+
import { isVersionRequest } from "./version-request.js";
|
|
4
5
|
import { isFastCoreRequest } from "./commands/scaffold-fast.js";
|
|
5
|
-
declare function isVersionRequest(argv: readonly string[]): boolean;
|
|
6
6
|
export declare const __test__: {
|
|
7
7
|
isVersionRequest: typeof isVersionRequest;
|
|
8
8
|
isFastCoreRequest: typeof isFastCoreRequest;
|
|
9
9
|
};
|
|
10
|
-
export {};
|
|
11
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -3,17 +3,13 @@
|
|
|
3
3
|
// Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
|
|
4
4
|
import "./node-preflight.js";
|
|
5
5
|
import "./node-network-compat.js";
|
|
6
|
-
import {
|
|
6
|
+
import { isVersionRequest } from "./version-request.js";
|
|
7
7
|
// Dependency-light: a pure parser + table, no command graph. Safe to load on
|
|
8
8
|
// every path (including --version) without reintroducing the heavy startup.
|
|
9
9
|
import { isFastCoreRequest } from "./commands/scaffold-fast.js";
|
|
10
10
|
// Also dependency-light: registers a process-level rejection boundary; its only
|
|
11
11
|
// heavy import (main.js) is lazy and reached only when a rejection fires.
|
|
12
12
|
import { installProcessRejectionBoundary } from "./unhandled-rejection-boundary.js";
|
|
13
|
-
function isVersionRequest(argv) {
|
|
14
|
-
const args = argv.slice(2);
|
|
15
|
-
return args.length === 1 && (args[0] === "--version" || args[0] === "-V" || args[0] === "-v");
|
|
16
|
-
}
|
|
17
13
|
// Install BEFORE any dispatch so a floated rejection on ANY path (the command
|
|
18
14
|
// graph or the fast-core forwarder) becomes a non-zero exit. @sentry/node's
|
|
19
15
|
// onUnhandledRejection integration defaults to 'warn' — it captures but does
|
|
@@ -22,7 +18,9 @@ function isVersionRequest(argv) {
|
|
|
22
18
|
// so runCli's finally still finalizes release health and flushes Sentry.
|
|
23
19
|
installProcessRejectionBoundary();
|
|
24
20
|
if (isVersionRequest(process.argv)) {
|
|
25
|
-
|
|
21
|
+
// Same report as `hq version`: HQ, CLI, latest hq-core. Lazy so --version
|
|
22
|
+
// still skips the command graph.
|
|
23
|
+
void import("./utils/version-report.js").then(({ runVersionCommand }) => runVersionCommand(process.argv));
|
|
26
24
|
}
|
|
27
25
|
else if (isFastCoreRequest(process.argv)) {
|
|
28
26
|
// Hot relocated plumbing (`hq core hq-session`, `hq core checkpoint-stop-gate`)
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import { at, basename, date, extractKeepFences, generatedFingerprint, generatedStamp, hasUnclosedKeepMarker, heading, immediateEntries, isHidden, log, looksAutoGenerated, readJson, readText, sanitize, stampGenerated, titleize, truncate, write, } from "./shared.js";
|
|
2
3
|
function describe(item) {
|
|
3
4
|
const name = basename(item);
|
|
4
|
-
// `find -L` follows a symlink only for its direct target. fs.statSync mirrors that.
|
|
5
5
|
try {
|
|
6
|
-
if ((
|
|
7
|
-
const
|
|
8
|
-
|
|
6
|
+
if (fs.statSync(item).isDirectory()) {
|
|
7
|
+
const entries = fs.readdirSync(item).filter((entry) => !entry.startsWith("."));
|
|
8
|
+
const n = entries.length;
|
|
9
|
+
const summary = heading(`${item}/README.md`);
|
|
10
|
+
return summary ? `${summary} (${n} item(s))` : `${n} item(s)`;
|
|
9
11
|
}
|
|
10
12
|
}
|
|
11
13
|
catch {
|
|
@@ -23,13 +25,55 @@ function describe(item) {
|
|
|
23
25
|
}
|
|
24
26
|
return name;
|
|
25
27
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
function briefDiff(existing, next) {
|
|
29
|
+
const oldLines = existing.split("\n");
|
|
30
|
+
const newLines = next.split("\n");
|
|
31
|
+
const lines = ["--- existing", "+++ generated"];
|
|
32
|
+
const limit = Math.max(oldLines.length, newLines.length);
|
|
33
|
+
let shown = 0;
|
|
34
|
+
for (let i = 0; i < limit && shown < 20; i += 1) {
|
|
35
|
+
const a = oldLines[i];
|
|
36
|
+
const b = newLines[i];
|
|
37
|
+
if (a === b)
|
|
38
|
+
continue;
|
|
39
|
+
if (a !== undefined)
|
|
40
|
+
lines.push(`- ${a}`);
|
|
41
|
+
if (b !== undefined)
|
|
42
|
+
lines.push(`+ ${b}`);
|
|
43
|
+
shown += 1;
|
|
44
|
+
}
|
|
45
|
+
if (shown === 20)
|
|
46
|
+
lines.push("…");
|
|
47
|
+
return lines.join("\n");
|
|
48
|
+
}
|
|
49
|
+
function composeIndex(raw, existing) {
|
|
50
|
+
const stamped = stampGenerated(raw);
|
|
51
|
+
const fences = existing ? extractKeepFences(existing) : "";
|
|
52
|
+
if (!fences)
|
|
53
|
+
return stamped;
|
|
54
|
+
return `${stamped.replace(/\n+$/, "\n")}\n${fences}\n`;
|
|
55
|
+
}
|
|
56
|
+
function shouldOverwrite(context, existing, next) {
|
|
57
|
+
if (!existing)
|
|
58
|
+
return "write";
|
|
59
|
+
if (context.force)
|
|
60
|
+
return "write";
|
|
61
|
+
if (hasUnclosedKeepMarker(existing))
|
|
62
|
+
return "skip";
|
|
63
|
+
const stored = generatedStamp(existing);
|
|
64
|
+
if (stored && stored === generatedFingerprint(existing))
|
|
65
|
+
return "write";
|
|
66
|
+
if (stored)
|
|
67
|
+
return "refuse";
|
|
68
|
+
if (looksAutoGenerated(existing))
|
|
69
|
+
return "write";
|
|
70
|
+
if (generatedFingerprint(existing) === generatedFingerprint(next))
|
|
71
|
+
return "write";
|
|
72
|
+
return "refuse";
|
|
73
|
+
}
|
|
31
74
|
export function renderCompanyKnowledge(context) {
|
|
32
75
|
const written = [];
|
|
76
|
+
let refused = 0;
|
|
33
77
|
for (const companyDir of immediateEntries(context.root, "companies", "dir")) {
|
|
34
78
|
const company = basename(companyDir);
|
|
35
79
|
if (company.startsWith("_") || isHidden(company))
|
|
@@ -37,24 +81,24 @@ export function renderCompanyKnowledge(context) {
|
|
|
37
81
|
const relative = `companies/${company}/knowledge`;
|
|
38
82
|
const knowledge = `${companyDir}/knowledge`;
|
|
39
83
|
try {
|
|
40
|
-
if (!
|
|
84
|
+
if (!fs.statSync(knowledge).isDirectory())
|
|
41
85
|
continue;
|
|
42
86
|
}
|
|
43
87
|
catch {
|
|
44
88
|
continue;
|
|
45
89
|
}
|
|
46
|
-
const entries =
|
|
90
|
+
const entries = fs.readdirSync(knowledge).flatMap((name) => {
|
|
47
91
|
const item = `${knowledge}/${name}`;
|
|
48
92
|
try {
|
|
49
|
-
return
|
|
93
|
+
return fs.statSync(item).isDirectory() ? [item] : [];
|
|
50
94
|
}
|
|
51
95
|
catch {
|
|
52
96
|
return [];
|
|
53
97
|
}
|
|
54
|
-
}).sort().concat(
|
|
98
|
+
}).sort().concat(fs.readdirSync(knowledge).flatMap((name) => {
|
|
55
99
|
const item = `${knowledge}/${name}`;
|
|
56
100
|
try {
|
|
57
|
-
return
|
|
101
|
+
return fs.statSync(item).isFile() ? [item] : [];
|
|
58
102
|
}
|
|
59
103
|
catch {
|
|
60
104
|
return [];
|
|
@@ -68,18 +112,37 @@ export function renderCompanyKnowledge(context) {
|
|
|
68
112
|
const value = truncate(sanitize(describe(item)), 100) || "—";
|
|
69
113
|
let directory = false;
|
|
70
114
|
try {
|
|
71
|
-
directory =
|
|
115
|
+
directory = fs.statSync(item).isDirectory();
|
|
72
116
|
}
|
|
73
117
|
catch { /* omitted */ }
|
|
74
118
|
lines.push(`| \`${name}${directory ? "/" : ""}\` | ${value} |`);
|
|
75
119
|
}
|
|
76
120
|
lines.push("");
|
|
77
121
|
const output = `${relative}/INDEX.md`;
|
|
78
|
-
|
|
122
|
+
const raw = lines.join("\n");
|
|
123
|
+
const existing = readText(at(context.root, output));
|
|
124
|
+
const next = composeIndex(raw, existing);
|
|
125
|
+
const decision = shouldOverwrite(context, existing, next);
|
|
126
|
+
if (decision === "skip") {
|
|
127
|
+
log(context, `rebuild-company-knowledge-index: skipped ${output} (<!-- hq:keep -->)`);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (decision === "refuse") {
|
|
131
|
+
refused += 1;
|
|
132
|
+
context.refused?.push(output);
|
|
133
|
+
log(context, `rebuild-company-knowledge-index: refused ${output} (hand-written or edited; differs from last generated output)`);
|
|
134
|
+
log(context, briefDiff(existing ?? "", next));
|
|
135
|
+
log(context, "Re-run with --accept to overwrite, or add <!-- hq:keep --> to opt out.");
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
write(context, output, next);
|
|
79
139
|
written.push(output);
|
|
80
140
|
log(context, `rebuild-company-knowledge-index: wrote ${output}`);
|
|
81
141
|
}
|
|
82
142
|
log(context, `rebuild-company-knowledge-index: regenerated ${written.length} knowledge INDEX.md file(s)`);
|
|
143
|
+
if (refused > 0) {
|
|
144
|
+
log(context, `rebuild-company-knowledge-index: refused to overwrite ${refused} hand-edited INDEX.md file(s)`);
|
|
145
|
+
}
|
|
83
146
|
return { written };
|
|
84
147
|
}
|
|
85
148
|
//# sourceMappingURL=company-knowledge.js.map
|
|
@@ -3,6 +3,10 @@ export type RenderContext = {
|
|
|
3
3
|
now?: Date;
|
|
4
4
|
log?: (message: string) => void;
|
|
5
5
|
output?: (message: string) => void;
|
|
6
|
+
/** Overwrite hand-edited INDEX.md files (`rebuild-index … --accept`). */
|
|
7
|
+
force?: boolean;
|
|
8
|
+
/** Relative paths that were not overwritten because they differ from last generated output. */
|
|
9
|
+
refused?: string[];
|
|
6
10
|
};
|
|
7
11
|
export type RenderResult = {
|
|
8
12
|
written: string[];
|
|
@@ -26,6 +30,14 @@ export declare function timestamp(now?: Date): string;
|
|
|
26
30
|
/** Write via a same-directory temporary file, then atomically replace the destination. */
|
|
27
31
|
export declare function atomicWrite(file: string, content: string): void;
|
|
28
32
|
export declare function write(context: RenderContext, relative: string, content: string): string;
|
|
33
|
+
/** Drop keep fences, the generated stamp, and the rolling date so two generated bodies can be compared. */
|
|
34
|
+
export declare function generatedFingerprint(content: string): string;
|
|
35
|
+
/** Insert a content stamp after the Auto-generated line. Idempotent. */
|
|
36
|
+
export declare function stampGenerated(content: string): string;
|
|
37
|
+
export declare function extractKeepFences(content: string): string;
|
|
38
|
+
export declare function hasUnclosedKeepMarker(content: string): boolean;
|
|
39
|
+
export declare function looksAutoGenerated(content: string): boolean;
|
|
40
|
+
export declare function generatedStamp(content: string): string | undefined;
|
|
29
41
|
export declare function log(context: RenderContext, message: string): void;
|
|
30
42
|
export declare function projectStatus(root: string, project: string, prdPath: string, fallback: string): string;
|
|
31
43
|
export declare function basename(file: string): string;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
1
2
|
import * as fs from "fs";
|
|
2
3
|
import * as os from "os";
|
|
3
4
|
import * as path from "path";
|
|
@@ -96,6 +97,42 @@ export function write(context, relative, content) {
|
|
|
96
97
|
atomicWrite(at(context.root, relative), content);
|
|
97
98
|
return relative;
|
|
98
99
|
}
|
|
100
|
+
const GENERATED_DATE_LINE = /^> Auto-generated\. Updated: \d{4}-\d{2}-\d{2}$/m;
|
|
101
|
+
const GENERATED_STAMP_LINE = /^<!-- hq-generated-sha256: [a-f0-9]{64} -->\n?/m;
|
|
102
|
+
function keepFencePattern() {
|
|
103
|
+
return /<!-- hq:keep -->\r?\n?[\s\S]*?<!-- \/hq:keep -->/g;
|
|
104
|
+
}
|
|
105
|
+
/** Drop keep fences, the generated stamp, and the rolling date so two generated bodies can be compared. */
|
|
106
|
+
export function generatedFingerprint(content) {
|
|
107
|
+
const normalized = content
|
|
108
|
+
.replace(keepFencePattern(), "")
|
|
109
|
+
.replace(GENERATED_STAMP_LINE, "")
|
|
110
|
+
.replace(GENERATED_DATE_LINE, "> Auto-generated. Updated: <date>")
|
|
111
|
+
.replace(/\n+$/, "\n");
|
|
112
|
+
return createHash("sha256").update(normalized, "utf8").digest("hex");
|
|
113
|
+
}
|
|
114
|
+
/** Insert a content stamp after the Auto-generated line. Idempotent. */
|
|
115
|
+
export function stampGenerated(content) {
|
|
116
|
+
const unstamped = content.replace(GENERATED_STAMP_LINE, "");
|
|
117
|
+
const hash = generatedFingerprint(unstamped);
|
|
118
|
+
return unstamped.replace(GENERATED_DATE_LINE, (line) => `${line}\n<!-- hq-generated-sha256: ${hash} -->`);
|
|
119
|
+
}
|
|
120
|
+
export function extractKeepFences(content) {
|
|
121
|
+
return (content.match(keepFencePattern()) ?? []).join("\n\n");
|
|
122
|
+
}
|
|
123
|
+
export function hasUnclosedKeepMarker(content) {
|
|
124
|
+
const opens = content.match(/<!-- hq:keep -->/g)?.length ?? 0;
|
|
125
|
+
if (opens === 0)
|
|
126
|
+
return false;
|
|
127
|
+
const closes = content.match(/<!-- \/hq:keep -->/g)?.length ?? 0;
|
|
128
|
+
return opens > closes;
|
|
129
|
+
}
|
|
130
|
+
export function looksAutoGenerated(content) {
|
|
131
|
+
return GENERATED_DATE_LINE.test(content);
|
|
132
|
+
}
|
|
133
|
+
export function generatedStamp(content) {
|
|
134
|
+
return content.match(/^<!-- hq-generated-sha256: ([a-f0-9]{64}) -->/m)?.[1];
|
|
135
|
+
}
|
|
99
136
|
export function log(context, message) { context.log?.(message); }
|
|
100
137
|
export function projectStatus(root, project, prdPath, fallback) {
|
|
101
138
|
const state = readJson(at(root, `workspace/orchestrator/${project}/state.json`));
|
package/dist/utils/people.d.ts
CHANGED
|
@@ -86,17 +86,10 @@ export type ResolveResult = {
|
|
|
86
86
|
/**
|
|
87
87
|
* Resolve a person NAME to their email, built on top of {@link searchPeople}.
|
|
88
88
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* 3. substring search over name/email/slug
|
|
94
|
-
*
|
|
95
|
-
* The first tier that yields any match decides the result:
|
|
96
|
-
* - exactly one match with an email → `found`
|
|
97
|
-
* - exactly one match, no email → `no_email`
|
|
98
|
-
* - more than one match → `ambiguous` (caller disambiguates)
|
|
99
|
-
* - no match in any tier → `not_found`
|
|
89
|
+
* Resolution is deliberately stricter than a convenience ranking. Every
|
|
90
|
+
* case-insensitive name, email, or slug substring match is a candidate; an
|
|
91
|
+
* exact name never silently selects one person when the query also names
|
|
92
|
+
* other people. The caller must disambiguate any multi-person result.
|
|
100
93
|
*/
|
|
101
94
|
export declare function resolveNameToEmail(people: PersonRecord[], name: string): ResolveResult;
|
|
102
95
|
//# sourceMappingURL=people.d.ts.map
|
package/dist/utils/people.js
CHANGED
|
@@ -122,31 +122,16 @@ export function searchPeople(people, keyword) {
|
|
|
122
122
|
/**
|
|
123
123
|
* Resolve a person NAME to their email, built on top of {@link searchPeople}.
|
|
124
124
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* 3. substring search over name/email/slug
|
|
130
|
-
*
|
|
131
|
-
* The first tier that yields any match decides the result:
|
|
132
|
-
* - exactly one match with an email → `found`
|
|
133
|
-
* - exactly one match, no email → `no_email`
|
|
134
|
-
* - more than one match → `ambiguous` (caller disambiguates)
|
|
135
|
-
* - no match in any tier → `not_found`
|
|
125
|
+
* Resolution is deliberately stricter than a convenience ranking. Every
|
|
126
|
+
* case-insensitive name, email, or slug substring match is a candidate; an
|
|
127
|
+
* exact name never silently selects one person when the query also names
|
|
128
|
+
* other people. The caller must disambiguate any multi-person result.
|
|
136
129
|
*/
|
|
137
130
|
export function resolveNameToEmail(people, name) {
|
|
138
131
|
const query = name.trim();
|
|
139
132
|
if (!query)
|
|
140
133
|
return { status: "not_found" };
|
|
141
|
-
const
|
|
142
|
-
const exactName = people.filter((p) => p.name.toLowerCase() === lowered);
|
|
143
|
-
const exactSlug = people.filter((p) => p.slug.toLowerCase() === lowered);
|
|
144
|
-
const substring = searchPeople(people, query);
|
|
145
|
-
const matches = exactName.length > 0
|
|
146
|
-
? exactName
|
|
147
|
-
: exactSlug.length > 0
|
|
148
|
-
? exactSlug
|
|
149
|
-
: substring;
|
|
134
|
+
const matches = searchPeople(people, query);
|
|
150
135
|
if (matches.length === 0)
|
|
151
136
|
return { status: "not_found" };
|
|
152
137
|
if (matches.length > 1)
|
|
@@ -347,6 +347,14 @@ declare function nudgeUpdateRecommended(decision: VersionCheckResponse, install?
|
|
|
347
347
|
* see what the USER's next invocation resolves.
|
|
348
348
|
*/
|
|
349
349
|
export declare function resolveHqOnPath(): string | null;
|
|
350
|
+
/**
|
|
351
|
+
* Pull the CLI version out of `hq --version` output.
|
|
352
|
+
*
|
|
353
|
+
* Older builds printed a bare semver. Current builds print a labelled report
|
|
354
|
+
* (`HQ …` / `CLI 5.119.12` / …). Convergence must accept both, otherwise a
|
|
355
|
+
* successful install looks like a PATH shadow.
|
|
356
|
+
*/
|
|
357
|
+
export declare function parseReportedCliVersion(stdout: string): string | null;
|
|
350
358
|
/** `<bin> --version` output (trimmed), or null on any failure/timeout. */
|
|
351
359
|
export declare function probeCliVersion(bin: string): string | null;
|
|
352
360
|
/**
|
|
@@ -810,6 +810,28 @@ export function resolveHqOnPath() {
|
|
|
810
810
|
return null;
|
|
811
811
|
}
|
|
812
812
|
}
|
|
813
|
+
/**
|
|
814
|
+
* Pull the CLI version out of `hq --version` output.
|
|
815
|
+
*
|
|
816
|
+
* Older builds printed a bare semver. Current builds print a labelled report
|
|
817
|
+
* (`HQ …` / `CLI 5.119.12` / …). Convergence must accept both, otherwise a
|
|
818
|
+
* successful install looks like a PATH shadow.
|
|
819
|
+
*/
|
|
820
|
+
export function parseReportedCliVersion(stdout) {
|
|
821
|
+
const text = stdout.trim();
|
|
822
|
+
if (!text)
|
|
823
|
+
return null;
|
|
824
|
+
const labelled = /^CLI\s+(\S+)/m.exec(text);
|
|
825
|
+
if (labelled)
|
|
826
|
+
return labelled[1];
|
|
827
|
+
const lines = text
|
|
828
|
+
.split(/\r?\n/)
|
|
829
|
+
.map((line) => line.trim())
|
|
830
|
+
.filter(Boolean);
|
|
831
|
+
if (lines.length === 1)
|
|
832
|
+
return lines[0];
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
813
835
|
/** `<bin> --version` output (trimmed), or null on any failure/timeout. */
|
|
814
836
|
export function probeCliVersion(bin) {
|
|
815
837
|
try {
|
|
@@ -819,8 +841,7 @@ export function probeCliVersion(bin) {
|
|
|
819
841
|
});
|
|
820
842
|
if (result.error || result.status !== 0)
|
|
821
843
|
return null;
|
|
822
|
-
|
|
823
|
-
return out || null;
|
|
844
|
+
return parseReportedCliVersion(result.stdout ?? "");
|
|
824
845
|
}
|
|
825
846
|
catch {
|
|
826
847
|
return null;
|
|
@@ -1140,7 +1161,12 @@ export async function enforceVersionGate(onUpdateRecommended, options = {}) {
|
|
|
1140
1161
|
* force-upgraded.
|
|
1141
1162
|
*/
|
|
1142
1163
|
export function shouldSkipGate(argv) {
|
|
1143
|
-
return argv.some((a) => a === "--version" ||
|
|
1164
|
+
return argv.some((a) => a === "--version" ||
|
|
1165
|
+
a === "-V" ||
|
|
1166
|
+
a === "-v" ||
|
|
1167
|
+
a === "--help" ||
|
|
1168
|
+
a === "-h" ||
|
|
1169
|
+
a === "version");
|
|
1144
1170
|
}
|
|
1145
1171
|
export const __test__ = {
|
|
1146
1172
|
CLIENT_ID,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export declare const HQ_CORE_LATEST_URL = "https://api.github.com/repos/indigoai-us/hq-core/releases/latest";
|
|
2
|
+
export declare const LATEST_FETCH_TIMEOUT_MS = 3000;
|
|
3
|
+
export type LatestLookup = "ok" | "network-error";
|
|
4
|
+
export interface VersionReport {
|
|
5
|
+
hq: string | null;
|
|
6
|
+
cli: string;
|
|
7
|
+
latestHqCore: string | null;
|
|
8
|
+
updateAvailable: boolean;
|
|
9
|
+
latestLookup: LatestLookup;
|
|
10
|
+
}
|
|
11
|
+
export interface VersionReportDeps {
|
|
12
|
+
cliVersion?: string;
|
|
13
|
+
hqRoot?: string;
|
|
14
|
+
readHq?: (hqRoot: string) => string | null;
|
|
15
|
+
fetchLatest?: () => Promise<string | null>;
|
|
16
|
+
}
|
|
17
|
+
export declare function normalizeHqCoreTag(tag: string): string | null;
|
|
18
|
+
export declare function fetchLatestHqCoreRelease(fetchImpl?: typeof fetch, timeoutMs?: number): Promise<string | null>;
|
|
19
|
+
export declare function collectVersionReport(deps?: VersionReportDeps): Promise<VersionReport>;
|
|
20
|
+
export declare function formatVersionReport(report: VersionReport): string;
|
|
21
|
+
export declare function versionReportJson(report: VersionReport): string;
|
|
22
|
+
export { isVersionRequest } from "../version-request.js";
|
|
23
|
+
export declare function wantsJson(argv: readonly string[]): boolean;
|
|
24
|
+
export declare function runVersionCommand(argv?: readonly string[], deps?: VersionReportDeps, stdout?: {
|
|
25
|
+
write(chunk: string): unknown;
|
|
26
|
+
}): Promise<void>;
|
|
27
|
+
//# sourceMappingURL=version-report.d.ts.map
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The payload behind `hq version` / labelled `hq --version`.
|
|
3
|
+
*
|
|
4
|
+
* Prints the installed HQ scaffold version, the CLI version (labelled so it
|
|
5
|
+
* cannot be confused with HQ), and the latest hq-core GitHub release. A
|
|
6
|
+
* network failure is an explicit line, never a silent omission.
|
|
7
|
+
*/
|
|
8
|
+
import semver from "semver";
|
|
9
|
+
import { CLI_VERSION } from "../cli-version.js";
|
|
10
|
+
import { findHqRoot } from "./manifest.js";
|
|
11
|
+
import { readHqVersion } from "./pack-contributions.js";
|
|
12
|
+
export const HQ_CORE_LATEST_URL = "https://api.github.com/repos/indigoai-us/hq-core/releases/latest";
|
|
13
|
+
export const LATEST_FETCH_TIMEOUT_MS = 3_000;
|
|
14
|
+
export function normalizeHqCoreTag(tag) {
|
|
15
|
+
const version = tag.trim().replace(/^v/i, "");
|
|
16
|
+
return semver.valid(version) ? version : null;
|
|
17
|
+
}
|
|
18
|
+
export async function fetchLatestHqCoreRelease(fetchImpl = fetch, timeoutMs = LATEST_FETCH_TIMEOUT_MS) {
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetchImpl(HQ_CORE_LATEST_URL, {
|
|
21
|
+
headers: {
|
|
22
|
+
Accept: "application/vnd.github+json",
|
|
23
|
+
"User-Agent": "hq-cli-version",
|
|
24
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
25
|
+
},
|
|
26
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
27
|
+
});
|
|
28
|
+
if (!res.ok)
|
|
29
|
+
return null;
|
|
30
|
+
const body = (await res.json());
|
|
31
|
+
if (typeof body.tag_name !== "string")
|
|
32
|
+
return null;
|
|
33
|
+
return normalizeHqCoreTag(body.tag_name);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export async function collectVersionReport(deps = {}) {
|
|
40
|
+
const cli = deps.cliVersion ?? CLI_VERSION;
|
|
41
|
+
let hq;
|
|
42
|
+
try {
|
|
43
|
+
const hqRoot = deps.hqRoot ?? findHqRoot();
|
|
44
|
+
hq = (deps.readHq ?? readHqVersion)(hqRoot);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
hq = null;
|
|
48
|
+
}
|
|
49
|
+
const latestHqCore = deps.fetchLatest
|
|
50
|
+
? await deps.fetchLatest()
|
|
51
|
+
: await fetchLatestHqCoreRelease();
|
|
52
|
+
const latestLookup = latestHqCore ? "ok" : "network-error";
|
|
53
|
+
const hqValid = hq ? semver.valid(hq) : null;
|
|
54
|
+
const latestValid = latestHqCore ? semver.valid(latestHqCore) : null;
|
|
55
|
+
const updateAvailable = Boolean(hqValid && latestValid && semver.gt(latestValid, hqValid));
|
|
56
|
+
return { hq, cli, latestHqCore, updateAvailable, latestLookup };
|
|
57
|
+
}
|
|
58
|
+
export function formatVersionReport(report) {
|
|
59
|
+
const lines = [
|
|
60
|
+
`HQ ${report.hq ?? "unknown"}`,
|
|
61
|
+
`CLI ${report.cli}`,
|
|
62
|
+
];
|
|
63
|
+
if (report.latestLookup === "network-error") {
|
|
64
|
+
lines.push("Latest hq-core unknown");
|
|
65
|
+
lines.push("could not reach the network to check for a newer HQ");
|
|
66
|
+
return `${lines.join("\n")}\n`;
|
|
67
|
+
}
|
|
68
|
+
lines.push(`Latest hq-core ${report.latestHqCore}`);
|
|
69
|
+
if (!report.hq) {
|
|
70
|
+
lines.push("installed HQ version unknown, cannot tell if an update is available");
|
|
71
|
+
}
|
|
72
|
+
else if (report.updateAvailable) {
|
|
73
|
+
lines.push("update available, run /update-hq");
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
lines.push("up to date");
|
|
77
|
+
}
|
|
78
|
+
return `${lines.join("\n")}\n`;
|
|
79
|
+
}
|
|
80
|
+
export function versionReportJson(report) {
|
|
81
|
+
return `${JSON.stringify({
|
|
82
|
+
hq: report.hq,
|
|
83
|
+
cli: report.cli,
|
|
84
|
+
latestHqCore: report.latestHqCore,
|
|
85
|
+
updateAvailable: report.updateAvailable,
|
|
86
|
+
latestLookup: report.latestLookup,
|
|
87
|
+
}, null, 2)}\n`;
|
|
88
|
+
}
|
|
89
|
+
export { isVersionRequest } from "../version-request.js";
|
|
90
|
+
export function wantsJson(argv) {
|
|
91
|
+
return argv.includes("--json");
|
|
92
|
+
}
|
|
93
|
+
export async function runVersionCommand(argv = process.argv, deps = {}, stdout = process.stdout) {
|
|
94
|
+
const report = await collectVersionReport(deps);
|
|
95
|
+
stdout.write(wantsJson(argv) ? versionReportJson(report) : formatVersionReport(report));
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=version-report.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argv predicate for the version fast path. Keep this file dependency-free so
|
|
3
|
+
* every `hq` invocation can classify `--version` / `version` without loading
|
|
4
|
+
* yaml, semver, or the command graph.
|
|
5
|
+
*/
|
|
6
|
+
export declare function isVersionRequest(argv: readonly string[]): boolean;
|
|
7
|
+
//# sourceMappingURL=version-request.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argv predicate for the version fast path. Keep this file dependency-free so
|
|
3
|
+
* every `hq` invocation can classify `--version` / `version` without loading
|
|
4
|
+
* yaml, semver, or the command graph.
|
|
5
|
+
*/
|
|
6
|
+
export function isVersionRequest(argv) {
|
|
7
|
+
const args = argv.slice(2);
|
|
8
|
+
if (args.length === 0)
|
|
9
|
+
return false;
|
|
10
|
+
if (args.includes("--help") || args.includes("-h"))
|
|
11
|
+
return false;
|
|
12
|
+
const positionals = args.filter((a) => !a.startsWith("-"));
|
|
13
|
+
const flags = args.filter((a) => a.startsWith("-"));
|
|
14
|
+
const allowedFlags = new Set(["--version", "-V", "-v", "--json"]);
|
|
15
|
+
if (!flags.every((f) => allowedFlags.has(f)))
|
|
16
|
+
return false;
|
|
17
|
+
if (positionals.length === 0) {
|
|
18
|
+
return flags.some((f) => f === "--version" || f === "-V" || f === "-v");
|
|
19
|
+
}
|
|
20
|
+
return positionals.length === 1 && positionals[0] === "version";
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=version-request.js.map
|