@indigoai-us/hq-cli 5.115.1 → 5.115.2
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 +10 -0
- package/assets/scaffold/core/scripts/qmd-reindex-after-sync.sh +10 -87
- package/dist/commands/bot.js +1 -1
- package/dist/commands/index-cmd.d.ts +8 -1
- package/dist/commands/index-cmd.js +15 -5
- package/dist/commands/onboard-identity-guard.d.ts +1 -1
- package/dist/commands/onboard.js +1 -1
- package/dist/lib/bot/prompt.d.ts +8 -1
- package/dist/lib/bot/prompt.js +10 -2
- package/dist/lib/bot/run.js +1 -1
- package/dist/lib/core-utils/qmd-reindex-after-sync.d.ts +4 -1
- package/dist/lib/core-utils/qmd-reindex-after-sync.js +11 -2
- package/dist/lib/onboarding/checkpoint.d.ts +13 -0
- package/dist/lib/onboarding/checkpoint.js +44 -0
- package/dist/lib/onboarding/cli/onboard.d.ts +53 -0
- package/dist/lib/onboarding/cli/onboard.js +135 -0
- package/dist/lib/onboarding/cli/prompts.d.ts +28 -0
- package/dist/lib/onboarding/cli/prompts.js +83 -0
- package/dist/lib/onboarding/errors.d.ts +33 -0
- package/dist/lib/onboarding/errors.js +59 -0
- package/dist/lib/onboarding/index.d.ts +17 -0
- package/dist/lib/onboarding/index.js +16 -0
- package/dist/lib/onboarding/orchestrator.d.ts +47 -0
- package/dist/lib/onboarding/orchestrator.js +569 -0
- package/dist/lib/onboarding/types.d.ts +79 -0
- package/dist/lib/onboarding/types.js +8 -0
- package/dist/lib/search-index/background.js +2 -200
- package/dist/lib/search-index/embed-lock.d.ts +38 -0
- package/dist/lib/search-index/embed-lock.js +286 -0
- package/dist/lib/search-index/index.d.ts +1 -0
- package/dist/lib/search-index/index.js +4 -1
- package/package.json +1 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.115.2] — 2026-09-15
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Search indexing no longer starts a second embedding rebuild when one is
|
|
10
|
+
already running. Two rebuilds could use much of a machine while writing to
|
|
11
|
+
the same local index, so hq now skips the duplicate and lets a later pass
|
|
12
|
+
finish any remaining work.
|
|
13
|
+
- Creating a company from the command line (`hq onboard create-company`), and from a bot's conversation, works again; it had been refused with "ownerUid must match the authenticated caller". A company you already created but never finished setting up is now picked up and completed instead of being refused as "already taken".
|
|
14
|
+
|
|
5
15
|
## [5.115.1] — 2026-09-15
|
|
6
16
|
|
|
7
17
|
### Fixed
|
|
@@ -1,93 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
2
|
+
# FORWARDER — the implementation of this script lives in the hq CLI.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# `qmd update` most recently. This script makes post-sync indexing automatic
|
|
8
|
-
# and deterministic, so every machine's personal index converges to the same
|
|
9
|
-
# content. The index itself stays personal (it is large, binary, and embeds
|
|
10
|
-
# absolute local paths); only the *freshness* is made automatic.
|
|
11
|
-
#
|
|
12
|
-
# Behavior:
|
|
13
|
-
# 1. Auto-registers any company knowledge dir that isn't yet a qmd
|
|
14
|
-
# collection (fixes the "newly-synced knowledge isn't searchable until I
|
|
15
|
-
# manually map it" gap).
|
|
16
|
-
# 2. Runs an incremental lexical reindex (`qmd update` — fast, skips
|
|
17
|
-
# unchanged files by mtime).
|
|
18
|
-
# 3. Rebuilds embeddings only when called with --embed (slow on a
|
|
19
|
-
# multi-GB index; meant for an idle/interval pass, not every sync).
|
|
20
|
-
#
|
|
21
|
-
# Idempotent and safe: no-op (exit 0) when qmd is absent or the path isn't an
|
|
22
|
-
# HQ root. Never blocks or fails a sync.
|
|
23
|
-
#
|
|
24
|
-
# Usage: qmd-reindex-after-sync.sh [hq_root] [--embed]
|
|
25
|
-
# hq_root Local HQ directory (default: $PWD). Must contain core/core.yaml.
|
|
26
|
-
# --embed Also rebuild embeddings (deferred-cost; omit for fast lexical-only).
|
|
27
|
-
|
|
28
|
-
set -uo pipefail
|
|
29
|
-
|
|
30
|
-
hq_root=""
|
|
31
|
-
embed=0
|
|
32
|
-
for arg in "$@"; do
|
|
33
|
-
case "$arg" in
|
|
34
|
-
--embed) embed=1 ;;
|
|
35
|
-
--*) : ;; # ignore unknown flags
|
|
36
|
-
*) [ -z "$hq_root" ] && hq_root="$arg" ;;
|
|
37
|
-
esac
|
|
38
|
-
done
|
|
39
|
-
[ -z "$hq_root" ] && hq_root="$PWD"
|
|
40
|
-
|
|
41
|
-
command -v qmd >/dev/null 2>&1 || exit 0
|
|
42
|
-
[ -f "$hq_root/core/core.yaml" ] || exit 0
|
|
43
|
-
|
|
44
|
-
cd "$hq_root" || exit 0
|
|
45
|
-
|
|
46
|
-
# 1. Auto-register company knowledge collections that don't exist yet.
|
|
47
|
-
# Convention matches /newcompany: --name <slug> --mask "**/*.md".
|
|
48
|
-
existing="$(qmd collection list 2>/dev/null || true)"
|
|
49
|
-
for kdir in companies/*/knowledge; do
|
|
50
|
-
[ -d "$kdir" ] || continue
|
|
51
|
-
[ -n "$(find "$kdir" -name '*.md' -not -name 'INDEX.md' -print -quit 2>/dev/null)" ] || continue
|
|
52
|
-
slug="$(basename "$(dirname "$kdir")")"
|
|
53
|
-
printf '%s\n' "$existing" | grep -Fq "qmd://$slug/" && continue
|
|
54
|
-
qmd collection add "$hq_root/$kdir" --name "$slug" --mask "**/*.md" >/dev/null 2>&1 || true
|
|
55
|
-
qmd context add "qmd://$slug" "Knowledge base for $slug." >/dev/null 2>&1 || true
|
|
56
|
-
done
|
|
57
|
-
|
|
58
|
-
# 1b. Auto-register company *projects* collections that don't exist yet.
|
|
59
|
-
# Convention matches the HQ-level `hq-projects` collection: --name <slug>-projects
|
|
60
|
-
# --mask "**/*.{md,json}" (so prd.json + project docs are searchable). Without
|
|
61
|
-
# this, company projects/ dirs are indexed by nothing and /startwork's global
|
|
62
|
-
# `qmd search "prd.json"` and /brainstorm's project discovery silently miss them.
|
|
63
|
-
for pdir in companies/*/projects; do
|
|
64
|
-
[ -d "$pdir" ] || continue
|
|
65
|
-
[ -n "$(find "$pdir" -type f \( -name '*.md' -o -name '*.json' \) -print -quit 2>/dev/null)" ] || continue
|
|
66
|
-
slug="$(basename "$(dirname "$pdir")")"
|
|
67
|
-
name="${slug}-projects"
|
|
68
|
-
printf '%s\n' "$existing" | grep -Fq "qmd://$name/" && continue
|
|
69
|
-
qmd collection add "$hq_root/$pdir" --name "$name" --mask "**/*.{md,json}" >/dev/null 2>&1 || true
|
|
70
|
-
qmd context add "qmd://$name" "Project PRDs and documentation for $slug." >/dev/null 2>&1 || true
|
|
71
|
-
done
|
|
72
|
-
|
|
73
|
-
# 1c. Auto-register the personal knowledge collection if it isn't one yet.
|
|
74
|
-
# personal/knowledge is now read DIRECTLY (the reindex symlink mirror into
|
|
75
|
-
# core/knowledge is retired), so it needs its own qmd collection to stay
|
|
76
|
-
# searchable rather than riding in under a core collection.
|
|
77
|
-
if [ -d "personal/knowledge" ] && \
|
|
78
|
-
[ -n "$(find "personal/knowledge" -name '*.md' -not -name 'INDEX.md' -print -quit 2>/dev/null)" ]; then
|
|
79
|
-
printf '%s\n' "$existing" | grep -Fq "qmd://personal-knowledge/" || {
|
|
80
|
-
qmd collection add "$hq_root/personal/knowledge" --name "personal-knowledge" --mask "**/*.md" >/dev/null 2>&1 || true
|
|
81
|
-
qmd context add "qmd://personal-knowledge" "Personal knowledge base (owner overlay)." >/dev/null 2>&1 || true
|
|
82
|
-
}
|
|
83
|
-
fi
|
|
4
|
+
# This shipped compatibility path must delegate rather than invoke qmd itself:
|
|
5
|
+
# `hq core qmd-reindex-after-sync` participates in hq-cli's shared embed lock.
|
|
6
|
+
# Arguments, stdio, exit status, and signal disposition are preserved.
|
|
84
7
|
|
|
85
|
-
|
|
86
|
-
qmd update >/dev/null 2>&1 || true
|
|
8
|
+
set -euo pipefail
|
|
87
9
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
10
|
+
if ! command -v hq >/dev/null 2>&1; then
|
|
11
|
+
echo "qmd-reindex-after-sync.sh: requires the hq CLI — this script's implementation now ships with it." >&2
|
|
12
|
+
echo "Install it with: npm install -g @indigoai-us/hq-cli" >&2
|
|
13
|
+
exit 127
|
|
91
14
|
fi
|
|
92
15
|
|
|
93
|
-
|
|
16
|
+
exec hq core qmd-reindex-after-sync "$@"
|
package/dist/commands/bot.js
CHANGED
|
@@ -800,7 +800,7 @@ async function runBotIntro(nameArg) {
|
|
|
800
800
|
if (hasPromotionHold(dir))
|
|
801
801
|
throw new Error("This bot is held for cloud promotion. Continue its promotion before changing local settings or lifecycle.");
|
|
802
802
|
const api = new BotApi({ token: botTokenSupplier(dir) });
|
|
803
|
-
await api.sendDm({ toPersonUid: config.ownerUid, body: introDmText(name, config.runtime, config.intro) });
|
|
803
|
+
await api.sendDm({ toPersonUid: config.ownerUid, body: introDmText(name, config.runtime, config.intro, { kind: effectiveBotKind(config), companies: effectiveBotCompanies(config) }) });
|
|
804
804
|
patchBotConfig(dir, { introSentAt: new Date().toISOString() });
|
|
805
805
|
console.log(chalk.green(`${name} introduced itself to you.`));
|
|
806
806
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { type RunQmdOptions, type SearchCollection, type QmdProcessResult } from '../lib/search-index/index.js';
|
|
3
3
|
import { type BackgroundDependencies, type BackgroundResult, type BackgroundStatus } from '../lib/search-index/background.js';
|
|
4
|
+
import { type EmbedLockDependencies } from '../lib/search-index/embed-lock.js';
|
|
4
5
|
export type SearchIndexDependencies = {
|
|
5
6
|
reconcileCollections: (hqRoot: string) => unknown;
|
|
6
7
|
/** Apply the per-document size cap to qmd's config before the update reads anything. */
|
|
@@ -13,9 +14,15 @@ export type SearchIndexDependencies = {
|
|
|
13
14
|
runBackgroundLauncher?: (dependencies: BackgroundDependencies) => BackgroundResult;
|
|
14
15
|
runBackgroundWorker?: (dependencies: BackgroundDependencies) => BackgroundResult | Promise<BackgroundResult>;
|
|
15
16
|
backgroundStatus?: (dependencies: BackgroundDependencies) => BackgroundStatus;
|
|
17
|
+
/** Test seams for the lock shared by every hq-cli embed entry point. */
|
|
18
|
+
embedLockDependencies?: EmbedLockDependencies;
|
|
19
|
+
writeStderr?: (text: string) => void;
|
|
20
|
+
};
|
|
21
|
+
export type SyncSearchIndexResult = {
|
|
22
|
+
embedded: boolean;
|
|
16
23
|
};
|
|
17
24
|
/** Incrementally update qmd, embedding only when an operator explicitly asks. */
|
|
18
|
-
export declare function syncSearchIndex(hqRoot: string, embed: boolean, dependencies?: SearchIndexDependencies):
|
|
25
|
+
export declare function syncSearchIndex(hqRoot: string, embed: boolean, dependencies?: SearchIndexDependencies): SyncSearchIndexResult;
|
|
19
26
|
export declare function collectionStatusLines(expected: SearchCollection[], registered: ReadonlySet<string>): string[];
|
|
20
27
|
export declare function collectionSummary(expected: SearchCollection[], registered: ReadonlySet<string>): string;
|
|
21
28
|
export declare function registerIndexCommand(program: Command, dependencies?: SearchIndexDependencies): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Option } from 'commander';
|
|
2
|
-
import { deriveCollections, listRegisteredCollections, packageLocalBin, reconcileCollections, resolveQmdBin, resolveQmdVersion, runQmd, } from '../lib/search-index/index.js';
|
|
2
|
+
import { deriveCollections, listRegisteredCollections, packageLocalBin, reconcileCollections, resolveQmdBin, resolveQmdHome, resolveQmdVersion, runQmd, } from '../lib/search-index/index.js';
|
|
3
3
|
import { backgroundStatus, defaultBackgroundDependencies, runBackgroundLauncher, runBackgroundWorker, } from '../lib/search-index/background.js';
|
|
4
|
+
import { defaultEmbedLockDependencies, runWithEmbedLock, } from '../lib/search-index/embed-lock.js';
|
|
4
5
|
import { applyIndexSizeLimit } from '../lib/search-index/max-doc-bytes.js';
|
|
5
6
|
import { findHqRoot } from '../utils/manifest.js';
|
|
6
7
|
import { QMD_NATIVE_BINDING_REMEDY, isQmdNativeBindingError, } from '../utils/qmd-native-binding-error.js';
|
|
@@ -35,8 +36,17 @@ export function syncSearchIndex(hqRoot, embed, dependencies = defaults) {
|
|
|
35
36
|
/* capping is an optimisation; indexing is the job */
|
|
36
37
|
}
|
|
37
38
|
dependencies.runQmd(['update'], { cwd: hqRoot });
|
|
38
|
-
if (embed)
|
|
39
|
-
|
|
39
|
+
if (!embed)
|
|
40
|
+
return { embedded: false };
|
|
41
|
+
const runEmbed = () => dependencies.runQmd(['embed'], { cwd: hqRoot });
|
|
42
|
+
const lockDependencies = dependencies.embedLockDependencies ?? defaultEmbedLockDependencies();
|
|
43
|
+
const outcome = runWithEmbedLock(resolveQmdHome(lockDependencies.env), lockDependencies, runEmbed);
|
|
44
|
+
const writeStderr = dependencies.writeStderr ?? ((text) => process.stderr.write(text));
|
|
45
|
+
if (outcome === 'busy')
|
|
46
|
+
writeStderr('hq: qmd embed is already running; skipping this pass.\n');
|
|
47
|
+
if (outcome === 'unavailable')
|
|
48
|
+
writeStderr('hq: cannot access the qmd embed lock; skipping this pass.\n');
|
|
49
|
+
return { embedded: outcome === 'ran' };
|
|
40
50
|
}
|
|
41
51
|
function resolveRoot(hqRoot) {
|
|
42
52
|
return hqRoot ?? findHqRoot();
|
|
@@ -73,8 +83,8 @@ export function registerIndexCommand(program, dependencies = defaults) {
|
|
|
73
83
|
.option('--hq-root <path>', 'HQ root to index (defaults to auto-detected root)')
|
|
74
84
|
.action((options) => {
|
|
75
85
|
const hqRoot = resolveRoot(options.hqRoot);
|
|
76
|
-
syncSearchIndex(hqRoot, options.embed === true);
|
|
77
|
-
console.log(`Updated search index for ${hqRoot}${
|
|
86
|
+
const result = syncSearchIndex(hqRoot, options.embed === true, dependencies);
|
|
87
|
+
console.log(`Updated search index for ${hqRoot}${result.embedded ? ' (including embeddings)' : ''}.`);
|
|
78
88
|
});
|
|
79
89
|
index
|
|
80
90
|
.command('collections')
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* the checkpoint adopted a person the caller no longer owns. The resume command
|
|
25
25
|
* uses it to surface a recoverable, actionable error instead of looping.
|
|
26
26
|
*/
|
|
27
|
-
import type { OnboardingCheckpoint } from "
|
|
27
|
+
import type { OnboardingCheckpoint } from "../lib/onboarding/index.js";
|
|
28
28
|
export type OnboardingIdentityCheck = {
|
|
29
29
|
kind: "ok";
|
|
30
30
|
} | {
|
package/dist/commands/onboard.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* browser-OAuth flow opens automatically.
|
|
20
20
|
*/
|
|
21
21
|
import chalk from "chalk";
|
|
22
|
-
import { runOnboardCli, readCheckpoint } from "
|
|
22
|
+
import { runOnboardCli, readCheckpoint } from "../lib/onboarding/index.js";
|
|
23
23
|
import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoToken, buildVaultConfig, } from "../utils/cognito-session.js";
|
|
24
24
|
import { createDefaultVaultClient } from "./cloud-provision.js";
|
|
25
25
|
import { detectOnboardingIdentityMismatch } from "./onboard-identity-guard.js";
|
package/dist/lib/bot/prompt.d.ts
CHANGED
|
@@ -110,6 +110,13 @@ export declare function nonOwnerRefusalText(botName: string, kind?: BotKind): st
|
|
|
110
110
|
/**
|
|
111
111
|
* The one-time introduction the bot DMs its owner when it first comes online.
|
|
112
112
|
* A bot created with `--intro` sends that text verbatim instead.
|
|
113
|
+
*
|
|
114
|
+
* The first sentence depends on the bot's kind: a personal bot runs under its
|
|
115
|
+
* owner's login and permissions; a company bot runs with its own identity as
|
|
116
|
+
* a member of its companies and can only work inside those.
|
|
113
117
|
*/
|
|
114
|
-
export declare function introDmText(botName: string, runtime: string, intro?: string
|
|
118
|
+
export declare function introDmText(botName: string, runtime: string, intro?: string, identity?: {
|
|
119
|
+
kind?: BotKind;
|
|
120
|
+
companies?: string[];
|
|
121
|
+
}): string;
|
|
115
122
|
//# sourceMappingURL=prompt.d.ts.map
|
package/dist/lib/bot/prompt.js
CHANGED
|
@@ -342,12 +342,20 @@ export function nonOwnerRefusalText(botName, kind = "personal") {
|
|
|
342
342
|
/**
|
|
343
343
|
* The one-time introduction the bot DMs its owner when it first comes online.
|
|
344
344
|
* A bot created with `--intro` sends that text verbatim instead.
|
|
345
|
+
*
|
|
346
|
+
* The first sentence depends on the bot's kind: a personal bot runs under its
|
|
347
|
+
* owner's login and permissions; a company bot runs with its own identity as
|
|
348
|
+
* a member of its companies and can only work inside those.
|
|
345
349
|
*/
|
|
346
|
-
export function introDmText(botName, runtime, intro) {
|
|
350
|
+
export function introDmText(botName, runtime, intro, identity = {}) {
|
|
347
351
|
if (intro?.trim())
|
|
348
352
|
return intro.trim();
|
|
349
353
|
const runtimeLabel = runtime === "claude" ? "Claude Code" : runtime === "codex" ? "Codex" : runtime === "grok" ? "Grok" : runtime;
|
|
350
|
-
|
|
354
|
+
const companies = (identity.companies ?? []).filter((c) => c.trim());
|
|
355
|
+
const opening = (identity.kind ?? "personal") === "company"
|
|
356
|
+
? `Hi, I'm ${botName} — your HQ company bot. I run locally on this computer with my own identity, as a member of ${companies.length > 0 ? companies.join(", ") : "my companies"}, so I can only work inside ${companies.length === 1 ? "that company" : "those companies"}.`
|
|
357
|
+
: `Hi, I'm ${botName} — your HQ bot. I run locally on this computer using your ${runtimeLabel} login, so I can work inside your HQ with your permissions.`;
|
|
358
|
+
return (`${opening}\n\n` +
|
|
351
359
|
`You can message me here from the desktop app or your phone whenever this computer is on, and I'll reply in this thread. ` +
|
|
352
360
|
`You can also open me in Claude Code or Codex from your HQ folder.\n\n` +
|
|
353
361
|
`What would you like me to do first?`);
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -298,7 +298,7 @@ export async function runBot(deps) {
|
|
|
298
298
|
}
|
|
299
299
|
else if (!config.introSentAt) {
|
|
300
300
|
try {
|
|
301
|
-
const sent = await api.sendDm({ toPersonUid: config.ownerUid, body: introDmText(config.name, config.runtime, config.intro) });
|
|
301
|
+
const sent = await api.sendDm({ toPersonUid: config.ownerUid, body: introDmText(config.name, config.runtime, config.intro, { kind: effectiveBotKind(config), companies: effectiveBotCompanies(config) }) });
|
|
302
302
|
patchBotConfig(dir, { introSentAt: now().toISOString() });
|
|
303
303
|
log("info", "intro DM sent to owner");
|
|
304
304
|
if (config.kickoff?.trim())
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type EmbedLockDependencies } from "../search-index/embed-lock.js";
|
|
2
|
+
import { type UtilityIo } from "./common.js";
|
|
2
3
|
export type QmdReindexOptions = UtilityIo & {
|
|
3
4
|
cwd?: string;
|
|
4
5
|
/** Test seams; default to the real search-index implementations. */
|
|
@@ -11,6 +12,8 @@ export type QmdReindexOptions = UtilityIo & {
|
|
|
11
12
|
cwd: string;
|
|
12
13
|
}) => unknown;
|
|
13
14
|
sizeLimit?: (hqRoot: string) => unknown;
|
|
15
|
+
/** Test seams for the lock shared by every hq-cli embed entry point. */
|
|
16
|
+
embedLockDependencies?: EmbedLockDependencies;
|
|
14
17
|
};
|
|
15
18
|
export declare function qmdReindexAfterSync(args?: string[], options?: QmdReindexOptions): number;
|
|
16
19
|
//# sourceMappingURL=qmd-reindex-after-sync.d.ts.map
|
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
13
|
import * as path from "node:path";
|
|
14
|
-
import { reconcileCollections, resolveQmdBin, runQmd } from "../search-index/index.js";
|
|
14
|
+
import { reconcileCollections, resolveQmdBin, resolveQmdHome, runQmd } from "../search-index/index.js";
|
|
15
15
|
import { applyIndexSizeLimit } from "../search-index/max-doc-bytes.js";
|
|
16
|
+
import { defaultEmbedLockDependencies, runWithEmbedLock, } from "../search-index/embed-lock.js";
|
|
17
|
+
import { ioFor, line } from "./common.js";
|
|
16
18
|
export function qmdReindexAfterSync(args = [], options = {}) {
|
|
17
19
|
let hqRoot = "";
|
|
18
20
|
let embed = false;
|
|
@@ -38,6 +40,7 @@ export function qmdReindexAfterSync(args = [], options = {}) {
|
|
|
38
40
|
const reconcile = options.reconcile ?? ((root, opts) => reconcileCollections(root, opts));
|
|
39
41
|
const run = options.run ?? ((argv, opts) => runQmd(argv, opts));
|
|
40
42
|
const sizeLimit = options.sizeLimit ?? ((root) => applyIndexSizeLimit(root));
|
|
43
|
+
const { stderr } = ioFor(options);
|
|
41
44
|
// Every qmd interaction is best-effort: the shell suffixed each with `|| true`.
|
|
42
45
|
try {
|
|
43
46
|
reconcile(hqRoot, { bin });
|
|
@@ -62,8 +65,14 @@ export function qmdReindexAfterSync(args = [], options = {}) {
|
|
|
62
65
|
/* never fail a sync on index freshness */
|
|
63
66
|
}
|
|
64
67
|
if (embed) {
|
|
68
|
+
const lockDependencies = options.embedLockDependencies ?? defaultEmbedLockDependencies();
|
|
65
69
|
try {
|
|
66
|
-
run(["embed"], { bin, cwd: hqRoot });
|
|
70
|
+
const outcome = runWithEmbedLock(resolveQmdHome(lockDependencies.env), lockDependencies, () => { run(["embed"], { bin, cwd: hqRoot }); });
|
|
71
|
+
if (outcome === "busy") {
|
|
72
|
+
line(stderr, "hq: qmd embed is already running; skipping this pass.");
|
|
73
|
+
}
|
|
74
|
+
if (outcome === "unavailable")
|
|
75
|
+
line(stderr, "hq: cannot access the qmd embed lock; skipping this pass.");
|
|
67
76
|
}
|
|
68
77
|
catch {
|
|
69
78
|
/* embeddings are deferred-cost and optional */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding checkpoint persistence (VLT-9 US-001; vendored from @indigoai-us/hq-onboarding).
|
|
3
|
+
*
|
|
4
|
+
* Reads/writes .hq/onboarding-state.json for idempotent resume.
|
|
5
|
+
* If a flow fails partway, re-running picks up from the last checkpoint.
|
|
6
|
+
*/
|
|
7
|
+
import type { OnboardingCheckpoint, OnboardingStep } from "./types.js";
|
|
8
|
+
export declare function getCheckpointPath(hqRoot: string): string;
|
|
9
|
+
export declare function readCheckpoint(hqRoot: string): Promise<OnboardingCheckpoint | null>;
|
|
10
|
+
export declare function writeCheckpoint(hqRoot: string, checkpoint: OnboardingCheckpoint): Promise<void>;
|
|
11
|
+
export declare function isStepComplete(checkpoint: OnboardingCheckpoint | null, step: OnboardingStep): boolean;
|
|
12
|
+
export declare function deleteCheckpoint(hqRoot: string): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=checkpoint.d.ts.map
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding checkpoint persistence (VLT-9 US-001; vendored from @indigoai-us/hq-onboarding).
|
|
3
|
+
*
|
|
4
|
+
* Reads/writes .hq/onboarding-state.json for idempotent resume.
|
|
5
|
+
* If a flow fails partway, re-running picks up from the last checkpoint.
|
|
6
|
+
*/
|
|
7
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
8
|
+
import { join, dirname } from "node:path";
|
|
9
|
+
const CHECKPOINT_FILE = "onboarding-state.json";
|
|
10
|
+
export function getCheckpointPath(hqRoot) {
|
|
11
|
+
return join(hqRoot, ".hq", CHECKPOINT_FILE);
|
|
12
|
+
}
|
|
13
|
+
export async function readCheckpoint(hqRoot) {
|
|
14
|
+
const path = getCheckpointPath(hqRoot);
|
|
15
|
+
try {
|
|
16
|
+
const raw = await readFile(path, "utf-8");
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function writeCheckpoint(hqRoot, checkpoint) {
|
|
24
|
+
const path = getCheckpointPath(hqRoot);
|
|
25
|
+
await mkdir(dirname(path), { recursive: true });
|
|
26
|
+
checkpoint.updatedAt = new Date().toISOString();
|
|
27
|
+
await writeFile(path, JSON.stringify(checkpoint, null, 2) + "\n", "utf-8");
|
|
28
|
+
}
|
|
29
|
+
export function isStepComplete(checkpoint, step) {
|
|
30
|
+
if (!checkpoint)
|
|
31
|
+
return false;
|
|
32
|
+
return checkpoint.completedSteps.includes(step);
|
|
33
|
+
}
|
|
34
|
+
export async function deleteCheckpoint(hqRoot) {
|
|
35
|
+
const { unlink } = await import("node:fs/promises");
|
|
36
|
+
const path = getCheckpointPath(hqRoot);
|
|
37
|
+
try {
|
|
38
|
+
await unlink(path);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// File may not exist — that's fine
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=checkpoint.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI onboard entry point (VLT-9 US-002; vendored from @indigoai-us/hq-onboarding).
|
|
3
|
+
*
|
|
4
|
+
* Programmatic API for the /onboard command. The slash command calls
|
|
5
|
+
* these functions; they can also be consumed by integration tests.
|
|
6
|
+
*/
|
|
7
|
+
import type { VaultServiceConfig } from "@indigoai-us/hq-cloud";
|
|
8
|
+
import { VaultClient } from "@indigoai-us/hq-cloud";
|
|
9
|
+
import type { OnboardingResult } from "../types.js";
|
|
10
|
+
export interface OnboardCliOptions {
|
|
11
|
+
mode: "create-company" | "join-company" | "resume" | "dry-run";
|
|
12
|
+
personName?: string;
|
|
13
|
+
personEmail?: string;
|
|
14
|
+
companyName?: string;
|
|
15
|
+
companySlug?: string;
|
|
16
|
+
inviteToken?: string;
|
|
17
|
+
vaultConfig: VaultServiceConfig;
|
|
18
|
+
hqRoot: string;
|
|
19
|
+
log?: (msg: string) => void;
|
|
20
|
+
}
|
|
21
|
+
export interface OnboardCliResult {
|
|
22
|
+
success: boolean;
|
|
23
|
+
result?: OnboardingResult;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
export type CompanySlugAvailability = {
|
|
27
|
+
kind: "available";
|
|
28
|
+
}
|
|
29
|
+
/** The caller already owns (or is a member of) a company with this slug. */
|
|
30
|
+
| {
|
|
31
|
+
kind: "mine";
|
|
32
|
+
uid: string;
|
|
33
|
+
}
|
|
34
|
+
/** Another account holds the slug. */
|
|
35
|
+
| {
|
|
36
|
+
kind: "taken";
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Decide whether `create-company` may proceed for a slug.
|
|
40
|
+
*
|
|
41
|
+
* A company that already exists in the CALLER's namespace is not "taken": it
|
|
42
|
+
* is theirs — typically left behind by an earlier run that created the
|
|
43
|
+
* entity and then died before the bucket, membership, or config were done
|
|
44
|
+
* (there may be no local checkpoint or company folder at all). The flow
|
|
45
|
+
* resumes against it instead of refusing. Only a slug held by a different
|
|
46
|
+
* account is refused.
|
|
47
|
+
*/
|
|
48
|
+
export declare function checkCompanySlugAvailability(client: Pick<VaultClient, "entity">, slug: string): Promise<CompanySlugAvailability>;
|
|
49
|
+
/**
|
|
50
|
+
* Run the /onboard CLI flow.
|
|
51
|
+
*/
|
|
52
|
+
export declare function runOnboardCli(options: OnboardCliOptions): Promise<OnboardCliResult>;
|
|
53
|
+
//# sourceMappingURL=onboard.d.ts.map
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI onboard entry point (VLT-9 US-002; vendored from @indigoai-us/hq-onboarding).
|
|
3
|
+
*
|
|
4
|
+
* Programmatic API for the /onboard command. The slash command calls
|
|
5
|
+
* these functions; they can also be consumed by integration tests.
|
|
6
|
+
*/
|
|
7
|
+
import { VaultClient, VaultConflictError, VaultNotFoundError } from "@indigoai-us/hq-cloud";
|
|
8
|
+
import { createCompanyFlow, joinCompanyFlow, resumeOnboarding, } from "../orchestrator.js";
|
|
9
|
+
import { readCheckpoint, getCheckpointPath } from "../checkpoint.js";
|
|
10
|
+
import { formatProgress, formatSummary, formatError } from "./prompts.js";
|
|
11
|
+
const CREATE_STEPS = 6;
|
|
12
|
+
const JOIN_STEPS = 6;
|
|
13
|
+
/**
|
|
14
|
+
* Decide whether `create-company` may proceed for a slug.
|
|
15
|
+
*
|
|
16
|
+
* A company that already exists in the CALLER's namespace is not "taken": it
|
|
17
|
+
* is theirs — typically left behind by an earlier run that created the
|
|
18
|
+
* entity and then died before the bucket, membership, or config were done
|
|
19
|
+
* (there may be no local checkpoint or company folder at all). The flow
|
|
20
|
+
* resumes against it instead of refusing. Only a slug held by a different
|
|
21
|
+
* account is refused.
|
|
22
|
+
*/
|
|
23
|
+
export async function checkCompanySlugAvailability(client, slug) {
|
|
24
|
+
const mine = await client.entity.findInMyNamespace("company", slug);
|
|
25
|
+
if (mine)
|
|
26
|
+
return { kind: "mine", uid: mine.uid };
|
|
27
|
+
try {
|
|
28
|
+
await client.entity.findBySlug("company", slug);
|
|
29
|
+
return { kind: "taken" };
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
// VaultNotFoundError = slug available, continue
|
|
33
|
+
const isNotFound = err instanceof VaultNotFoundError ||
|
|
34
|
+
(err instanceof Error && err.name === "VaultNotFoundError");
|
|
35
|
+
if (isNotFound)
|
|
36
|
+
return { kind: "available" };
|
|
37
|
+
// Several other tenants hold the slug (server answers 409): not ours.
|
|
38
|
+
const isConflict = err instanceof VaultConflictError ||
|
|
39
|
+
(err instanceof Error && err.name === "VaultConflictError");
|
|
40
|
+
if (isConflict)
|
|
41
|
+
return { kind: "taken" };
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Run the /onboard CLI flow.
|
|
47
|
+
*/
|
|
48
|
+
export async function runOnboardCli(options) {
|
|
49
|
+
const { mode, vaultConfig, hqRoot, log = console.log } = options;
|
|
50
|
+
let stepCounter = 0;
|
|
51
|
+
const onProgress = (event) => {
|
|
52
|
+
if (event.status === "running")
|
|
53
|
+
stepCounter++;
|
|
54
|
+
const total = mode === "create-company" ? CREATE_STEPS : JOIN_STEPS;
|
|
55
|
+
log(formatProgress(event, stepCounter, total));
|
|
56
|
+
};
|
|
57
|
+
const config = { vaultConfig, hqRoot };
|
|
58
|
+
try {
|
|
59
|
+
if (mode === "resume") {
|
|
60
|
+
const checkpoint = await readCheckpoint(hqRoot);
|
|
61
|
+
if (!checkpoint) {
|
|
62
|
+
return { success: false, error: "No checkpoint found. Run /onboard to start." };
|
|
63
|
+
}
|
|
64
|
+
log(`Resuming ${checkpoint.mode} flow from step ${checkpoint.completedSteps.length + 1}...`);
|
|
65
|
+
const result = await resumeOnboarding(config, onProgress);
|
|
66
|
+
log("");
|
|
67
|
+
log(formatSummary(result));
|
|
68
|
+
return { success: true, result };
|
|
69
|
+
}
|
|
70
|
+
if (mode === "dry-run") {
|
|
71
|
+
log("DRY RUN — simulating create-company flow:");
|
|
72
|
+
log(" 1. Create person entity");
|
|
73
|
+
log(" 2. Create company entity");
|
|
74
|
+
log(" 3. Provision S3 bucket + KMS key");
|
|
75
|
+
log(" 4. Bootstrap owner membership");
|
|
76
|
+
log(" 5. Verify STS credential vending");
|
|
77
|
+
log(" 6. Write .hq/config.json");
|
|
78
|
+
log("");
|
|
79
|
+
log("No resources will be created. Run /onboard to execute.");
|
|
80
|
+
return { success: true };
|
|
81
|
+
}
|
|
82
|
+
if (mode === "create-company") {
|
|
83
|
+
// Validate slug availability
|
|
84
|
+
if (options.companySlug) {
|
|
85
|
+
const client = new VaultClient(vaultConfig);
|
|
86
|
+
const availability = await checkCompanySlugAvailability(client, options.companySlug);
|
|
87
|
+
if (availability.kind === "taken") {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
error: `Company slug "${options.companySlug}" is already taken. Choose another.`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (availability.kind === "mine") {
|
|
94
|
+
log(`Company "${options.companySlug}" already exists in your account (${availability.uid}) — finishing its setup.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const input = {
|
|
98
|
+
mode: "create-company",
|
|
99
|
+
personName: options.personName,
|
|
100
|
+
personEmail: options.personEmail,
|
|
101
|
+
companyName: options.companyName,
|
|
102
|
+
companySlug: options.companySlug,
|
|
103
|
+
};
|
|
104
|
+
log(`Creating company "${input.companyName}" (${input.companySlug})...`);
|
|
105
|
+
log("");
|
|
106
|
+
const result = await createCompanyFlow(input, config, onProgress);
|
|
107
|
+
log("");
|
|
108
|
+
log(formatSummary(result));
|
|
109
|
+
return { success: true, result };
|
|
110
|
+
}
|
|
111
|
+
if (mode === "join-company") {
|
|
112
|
+
const input = {
|
|
113
|
+
mode: "join-company",
|
|
114
|
+
personName: options.personName,
|
|
115
|
+
personEmail: options.personEmail,
|
|
116
|
+
inviteToken: options.inviteToken,
|
|
117
|
+
};
|
|
118
|
+
log("Joining company via invite...");
|
|
119
|
+
log("");
|
|
120
|
+
const result = await joinCompanyFlow(input, config, onProgress);
|
|
121
|
+
log("");
|
|
122
|
+
log(formatSummary(result));
|
|
123
|
+
return { success: true, result };
|
|
124
|
+
}
|
|
125
|
+
return { success: false, error: `Unknown mode: ${mode}` };
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
const checkpointPath = getCheckpointPath(hqRoot);
|
|
129
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
130
|
+
log("");
|
|
131
|
+
log(formatError(err instanceof Error ? err : new Error(errorMsg), checkpointPath));
|
|
132
|
+
return { success: false, error: errorMsg };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=onboard.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI prompt helpers for /onboard command (VLT-9 US-002; vendored from @indigoai-us/hq-onboarding).
|
|
3
|
+
*
|
|
4
|
+
* Provides typed prompt interfaces the slash command can call.
|
|
5
|
+
* These are library functions — the actual UX is in onboard.md.
|
|
6
|
+
*/
|
|
7
|
+
import type { OnboardingResult, OnboardingProgress } from "../types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Format a progress event for CLI display.
|
|
10
|
+
*/
|
|
11
|
+
export declare function formatProgress(event: OnboardingProgress, stepNumber: number, totalSteps: number): string;
|
|
12
|
+
/**
|
|
13
|
+
* Format the success summary box.
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatSummary(result: OnboardingResult): string;
|
|
16
|
+
/**
|
|
17
|
+
* Format an error for CLI display with recovery hints.
|
|
18
|
+
*/
|
|
19
|
+
export declare function formatError(error: Error, checkpointPath: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Validate a company slug: lowercase, alphanumeric + hyphens, 3-40 chars.
|
|
22
|
+
*/
|
|
23
|
+
export declare function validateSlug(slug: string): string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Validate email format.
|
|
26
|
+
*/
|
|
27
|
+
export declare function validateEmail(email: string): string | null;
|
|
28
|
+
//# sourceMappingURL=prompts.d.ts.map
|