@indigoai-us/hq-cli 5.108.22 → 5.108.24

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.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Interactive per-repo company prompt (Work Mesh Live gap 4 — person attribution).
3
+ *
4
+ * Owner decision ("Always ask per repo"): every person is asked ONCE PER REPO
5
+ * which company the work is filed under, then the answer is remembered for that
6
+ * repo (persisted repo→company map in the device config).
7
+ *
8
+ * Hard gates — the prompt NEVER fires:
9
+ * - on machine / agent-box identities (isMachineIdentity) — those resolve from
10
+ * the identity file, never a prompt;
11
+ * - when stdin/stdout is not a TTY, or in --json / --machine / non-interactive
12
+ * contexts;
13
+ * - when cwd is not inside a git repo (no stable key to remember an answer);
14
+ * - when the repo is already mapped (a second resolve is silent).
15
+ *
16
+ * There is NO auto-pick from a sole membership: even a caller with exactly one
17
+ * active membership is asked once. The core is dependency-injected so tests never
18
+ * need a real TTY, network, or identity file.
19
+ */
20
+ import { setRepoCompany, getRepoCompany } from "./config.js";
21
+ import { deriveRepoIdentityKey } from "./repo-remote.js";
22
+ function buildQuestion(companies) {
23
+ const lines = companies.map((c, i) => ` ${i + 1}) ${c.companySlug}${c.companyUid ? ` (${c.companyUid})` : ""}`);
24
+ return [
25
+ "Which company is this repo's work filed under?",
26
+ ...lines,
27
+ "Enter a number (or company slug): ",
28
+ ].join("\n");
29
+ }
30
+ /** Match a raw answer to a membership by 1-based number or slug/uid. */
31
+ export function matchMembershipAnswer(answer, companies) {
32
+ const trimmed = answer.trim();
33
+ if (!trimmed)
34
+ return null;
35
+ if (/^\d+$/.test(trimmed)) {
36
+ const idx = Number.parseInt(trimmed, 10) - 1;
37
+ return idx >= 0 && idx < companies.length ? companies[idx] : null;
38
+ }
39
+ const needle = trimmed.toLowerCase();
40
+ const matches = companies.filter((c) => c.companySlug.toLowerCase() === needle ||
41
+ c.companyUid.toLowerCase() === needle);
42
+ return matches.length === 1 ? matches[0] : null;
43
+ }
44
+ /**
45
+ * Resolve a repo's company via the one-time interactive prompt (gap 4).
46
+ * Pure control flow over injected effects — no direct TTY/network/fs beyond the
47
+ * default config helpers. Callers must first confirm the session is unresolved
48
+ * (needs_company); this only decides whether/how to prompt and persist.
49
+ */
50
+ export async function promptRepoCompany(deps) {
51
+ // Gate 1: machine / agent-box identities never prompt.
52
+ if (deps.isMachineIdentity()) {
53
+ return { status: "skipped", reason: "machine" };
54
+ }
55
+ // Gate 2: only interactive person TTYs prompt.
56
+ if (!deps.isInteractive()) {
57
+ return { status: "skipped", reason: "non_interactive" };
58
+ }
59
+ const deriveKey = deps.deriveKey ?? ((cwd) => deriveRepoIdentityKey({ cwd }));
60
+ const repoKey = deriveKey(deps.cwd);
61
+ // Gate 3: no repo → nothing stable to remember; do not prompt.
62
+ if (!repoKey) {
63
+ return { status: "skipped", reason: "no_repo_key" };
64
+ }
65
+ // Already remembered → silent resolve (a second resolve never re-asks).
66
+ const lookup = deps.lookup ?? ((key, root) => getRepoCompany(key, { root }));
67
+ const existing = lookup(repoKey, deps.root);
68
+ if (existing?.slug) {
69
+ return {
70
+ status: "resolved",
71
+ repoKey,
72
+ company: { slug: existing.slug, uid: existing.uid },
73
+ persisted: false,
74
+ alreadyMapped: true,
75
+ };
76
+ }
77
+ const memberships = await deps.listMemberships();
78
+ if (!memberships || memberships.length === 0) {
79
+ return { status: "skipped", reason: "no_memberships", repoKey };
80
+ }
81
+ // Always ask — NO auto-pick, even for a sole membership.
82
+ const answer = await deps.ask(buildQuestion(memberships));
83
+ const chosen = matchMembershipAnswer(answer, memberships);
84
+ if (!chosen) {
85
+ return { status: "skipped", reason: "cancelled", repoKey };
86
+ }
87
+ const persist = deps.persist ??
88
+ ((key, mapping, root) => setRepoCompany(key, mapping, { root, now: deps.now }));
89
+ persist(repoKey, { slug: chosen.companySlug, uid: chosen.companyUid }, deps.root);
90
+ return {
91
+ status: "resolved",
92
+ repoKey,
93
+ company: { slug: chosen.companySlug, uid: chosen.companyUid },
94
+ persisted: true,
95
+ alreadyMapped: false,
96
+ };
97
+ }
98
+ //# sourceMappingURL=repo-prompt.js.map
@@ -30,6 +30,23 @@ export declare function readOriginRemoteUrl(gitDir: string): string | null;
30
30
  */
31
31
  export declare function matchCompanySlugForRepo(ownerName: string, companies: CompaniesManifestMap): string | null;
32
32
  export declare function readCompaniesManifestMap(hqRoot: string): CompaniesManifestMap | null;
33
+ /**
34
+ * Walk up from startDir to the working-tree root (the dir that *contains* a
35
+ * `.git` entry). Unlike findEnclosingGitDir this returns the work tree, not the
36
+ * gitdir, so it is a stable per-repo identity for the fallback repo key.
37
+ */
38
+ export declare function findWorkTreeRoot(startDir: string): string | null;
39
+ /**
40
+ * Stable per-repo identity key for the persisted repo→company map (gap 4).
41
+ *
42
+ * Prefers the normalised git-remote `remote:owner/name` (lower-cased) so a repo
43
+ * keeps one mapping across clones/worktrees; falls back to `root:<abs work-tree>`
44
+ * when there is no recognised origin. Returns null when cwd is not inside a repo.
45
+ * Never runs git; never throws.
46
+ */
47
+ export declare function deriveRepoIdentityKey(opts: {
48
+ cwd?: string;
49
+ }): string | null;
33
50
  /**
34
51
  * Derive the deterministic remote-owner company slug from cwd + HQ manifest.
35
52
  * Returns null when there is no unique match.
@@ -173,6 +173,51 @@ export function readCompaniesManifestMap(hqRoot) {
173
173
  return null;
174
174
  }
175
175
  }
176
+ /**
177
+ * Walk up from startDir to the working-tree root (the dir that *contains* a
178
+ * `.git` entry). Unlike findEnclosingGitDir this returns the work tree, not the
179
+ * gitdir, so it is a stable per-repo identity for the fallback repo key.
180
+ */
181
+ export function findWorkTreeRoot(startDir) {
182
+ let cur = path.resolve(startDir);
183
+ for (;;) {
184
+ try {
185
+ if (fs.existsSync(path.join(cur, ".git")))
186
+ return cur;
187
+ }
188
+ catch {
189
+ return null;
190
+ }
191
+ const parent = path.dirname(cur);
192
+ if (parent === cur)
193
+ return null;
194
+ cur = parent;
195
+ }
196
+ }
197
+ /**
198
+ * Stable per-repo identity key for the persisted repo→company map (gap 4).
199
+ *
200
+ * Prefers the normalised git-remote `remote:owner/name` (lower-cased) so a repo
201
+ * keeps one mapping across clones/worktrees; falls back to `root:<abs work-tree>`
202
+ * when there is no recognised origin. Returns null when cwd is not inside a repo.
203
+ * Never runs git; never throws.
204
+ */
205
+ export function deriveRepoIdentityKey(opts) {
206
+ const cwd = opts.cwd?.trim() ? path.resolve(opts.cwd) : process.cwd();
207
+ const gitDir = findEnclosingGitDir(cwd);
208
+ if (gitDir) {
209
+ const origin = readOriginRemoteUrl(gitDir);
210
+ if (origin) {
211
+ const ownerName = normalizeRemoteOwnerName(origin);
212
+ if (ownerName)
213
+ return `remote:${ownerName.toLowerCase()}`;
214
+ }
215
+ }
216
+ const workTree = findWorkTreeRoot(cwd);
217
+ if (workTree)
218
+ return `root:${workTree}`;
219
+ return null;
220
+ }
176
221
  /**
177
222
  * Derive the deterministic remote-owner company slug from cwd + HQ manifest.
178
223
  * Returns null when there is no unique match.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.108.22",
3
+ "version": "5.108.24",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -31,7 +31,7 @@
31
31
  "dependencies": {
32
32
  "@aws-sdk/client-iot-data-plane": "^3.1096.0",
33
33
  "@aws-sdk/client-s3": "^3.1049.0",
34
- "@indigoai-us/hq-cloud": "~6.16.23",
34
+ "@indigoai-us/hq-cloud": "~6.16.28",
35
35
  "@indigoai-us/hq-flags-client": "^0.1.2",
36
36
  "@indigoai-us/hq-onboarding": "^0.1.0",
37
37
  "@sentry/node": "^10.49.0",