@indigoai-us/hq-cli 5.60.0 → 5.62.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.
Files changed (98) hide show
  1. package/dist/commands/agents.d.ts +109 -0
  2. package/dist/commands/agents.js +385 -0
  3. package/dist/commands/db-migrate.d.ts +6 -0
  4. package/dist/commands/db-migrate.js +42 -0
  5. package/dist/commands/db-provision.d.ts +15 -0
  6. package/dist/commands/db-provision.js +78 -0
  7. package/dist/commands/db-sql.d.ts +9 -0
  8. package/dist/commands/db-sql.js +81 -0
  9. package/dist/commands/db-status.d.ts +7 -0
  10. package/dist/commands/db-status.js +70 -0
  11. package/dist/commands/db.d.ts +9 -0
  12. package/dist/commands/db.js +23 -0
  13. package/dist/commands/integrations.d.ts +78 -0
  14. package/dist/commands/integrations.js +309 -0
  15. package/dist/commands/members.js +4 -4
  16. package/dist/commands/outposts.d.ts +60 -0
  17. package/dist/commands/outposts.js +255 -0
  18. package/dist/commands/pack-install.d.ts +7 -1
  19. package/dist/commands/pack-install.js +86 -15
  20. package/dist/commands/packs.d.ts +2 -1
  21. package/dist/commands/packs.js +13 -8
  22. package/dist/commands/secrets.d.ts +13 -0
  23. package/dist/commands/secrets.js +149 -10
  24. package/dist/commands/skill.d.ts +153 -0
  25. package/dist/commands/skill.js +593 -0
  26. package/dist/commands/workers.d.ts +48 -0
  27. package/dist/commands/workers.js +229 -0
  28. package/dist/index.d.ts +5 -3
  29. package/dist/index.js +14 -240
  30. package/dist/lib/db/control-plane.d.ts +45 -0
  31. package/dist/lib/db/control-plane.js +81 -0
  32. package/dist/lib/db/local.d.ts +49 -0
  33. package/dist/lib/db/local.js +106 -0
  34. package/dist/lib/db/migrate.d.ts +41 -0
  35. package/dist/lib/db/migrate.js +104 -0
  36. package/dist/lib/db/paths.d.ts +56 -0
  37. package/dist/lib/db/paths.js +103 -0
  38. package/dist/lib/db/remote-engine.d.ts +58 -0
  39. package/dist/lib/db/remote-engine.js +90 -0
  40. package/dist/lib/db/remote-sql.d.ts +22 -0
  41. package/dist/lib/db/remote-sql.js +39 -0
  42. package/dist/lib/db/sql.d.ts +49 -0
  43. package/dist/lib/db/sql.js +132 -0
  44. package/dist/main.d.ts +7 -0
  45. package/dist/main.js +272 -0
  46. package/dist/utils/cognito-session.js +3 -3
  47. package/dist/utils/sandbox-runner-client.d.ts +13 -0
  48. package/dist/utils/sandbox-runner-client.js +83 -6
  49. package/dist/utils/version-check.d.ts +6 -0
  50. package/dist/utils/version-check.js +78 -2
  51. package/package.json +9 -1
  52. package/pnpm-workspace.yaml +2 -0
  53. package/src/commands/agents.test.ts +297 -0
  54. package/src/commands/agents.ts +561 -0
  55. package/src/commands/db-migrate.ts +55 -0
  56. package/src/commands/db-provision.ts +102 -0
  57. package/src/commands/db-sql.ts +124 -0
  58. package/src/commands/db-status.ts +100 -0
  59. package/src/commands/db.ts +26 -0
  60. package/src/commands/integrations.test.ts +284 -0
  61. package/src/commands/integrations.ts +438 -0
  62. package/src/commands/members.ts +2 -2
  63. package/src/commands/outposts.test.ts +177 -0
  64. package/src/commands/outposts.ts +338 -0
  65. package/src/commands/pack-install.ts +115 -18
  66. package/src/commands/pack-update-cache.test.ts +149 -0
  67. package/src/commands/packs.ts +28 -7
  68. package/src/commands/secrets.parse-destination.test.ts +38 -0
  69. package/src/commands/secrets.test.ts +342 -0
  70. package/src/commands/secrets.ts +227 -13
  71. package/src/commands/skill.test.ts +770 -0
  72. package/src/commands/skill.ts +796 -0
  73. package/src/commands/workers.test.ts +158 -0
  74. package/src/commands/workers.ts +298 -0
  75. package/src/index.test.ts +32 -0
  76. package/src/index.ts +11 -274
  77. package/src/lib/db/control-plane.test.ts +59 -0
  78. package/src/lib/db/control-plane.ts +113 -0
  79. package/src/lib/db/local.test.ts +81 -0
  80. package/src/lib/db/local.ts +148 -0
  81. package/src/lib/db/migrate.test.ts +133 -0
  82. package/src/lib/db/migrate.ts +137 -0
  83. package/src/lib/db/paths.test.ts +112 -0
  84. package/src/lib/db/paths.ts +128 -0
  85. package/src/lib/db/remote-engine.test.ts +44 -0
  86. package/src/lib/db/remote-engine.ts +148 -0
  87. package/src/lib/db/remote-sql.test.ts +32 -0
  88. package/src/lib/db/remote-sql.ts +62 -0
  89. package/src/lib/db/sql.test.ts +106 -0
  90. package/src/lib/db/sql.ts +192 -0
  91. package/src/main.ts +314 -0
  92. package/src/utils/cognito-session.ts +1 -1
  93. package/src/utils/sandbox-runner-client.test.ts +128 -0
  94. package/src/utils/sandbox-runner-client.ts +100 -4
  95. package/src/utils/version-check.test.ts +30 -0
  96. package/src/utils/version-check.ts +72 -0
  97. package/test/commands/db-tenant-isolation.test.ts +94 -0
  98. package/test/commands/db.test.ts +85 -0
@@ -8,7 +8,9 @@ import { CLI_VERSION } from "../cli-version.js";
8
8
  const PACKAGE_NAME = "@indigoai-us/hq-cli";
9
9
  const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
10
10
  const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
11
+ const CACHE_TTL_JITTER_MS = 60 * 60 * 1000;
11
12
  const FETCH_TIMEOUT_MS = 3_000;
13
+ const REFRESH_LOCK_STALE_MS = 10 * 60 * 1000;
12
14
 
13
15
  interface CacheEntry {
14
16
  latest: string;
@@ -19,6 +21,10 @@ function cachePath(): string {
19
21
  return path.join(os.homedir(), ".hq", "version-check.json");
20
22
  }
21
23
 
24
+ function lockPath(): string {
25
+ return path.join(os.homedir(), ".hq", "version-check.lock");
26
+ }
27
+
22
28
  function isOptedOut(): boolean {
23
29
  return process.env.HQ_NO_UPDATE_CHECK === "1";
24
30
  }
@@ -49,6 +55,58 @@ function writeCache(entry: CacheEntry): void {
49
55
  }
50
56
  }
51
57
 
58
+ function freshEnough(entry: CacheEntry, now = Date.now()): boolean {
59
+ const jitter = Math.floor(Math.random() * CACHE_TTL_JITTER_MS);
60
+ return now - entry.fetchedAt <= CACHE_TTL_MS - jitter;
61
+ }
62
+
63
+ function isKnownNoninteractiveStatusProbe(argv: readonly string[] = process.argv): boolean {
64
+ const args = argv.slice(2);
65
+ const positional = args.filter((arg) => !arg.startsWith("-"));
66
+ const json = args.includes("--json") || !process.stdout.isTTY;
67
+ if (!json) return false;
68
+ if (positional[0] === "mcp" && positional[1] === "status") return true;
69
+ if (positional[0] === "packs" && (positional[1] === "list" || positional[1] === "ls")) return true;
70
+ if (
71
+ positional[0] === "packages" &&
72
+ positional[1] === "packs" &&
73
+ (positional[2] === "list" || positional[2] === "ls")
74
+ ) {
75
+ return true;
76
+ }
77
+ if ((positional[0] === "sources" || positional[0] === "signals") && positional[1] === "list") {
78
+ return true;
79
+ }
80
+ return false;
81
+ }
82
+
83
+ function acquireRefreshLock(now = Date.now()): (() => void) | null {
84
+ const dir = lockPath();
85
+ try {
86
+ fs.mkdirSync(path.dirname(dir), { recursive: true });
87
+ fs.mkdirSync(dir);
88
+ fs.writeFileSync(path.join(dir, "owner"), `${process.pid}\n${now}\n`);
89
+ return () => {
90
+ try {
91
+ fs.rmSync(dir, { recursive: true, force: true });
92
+ } catch {
93
+ // best-effort lock cleanup
94
+ }
95
+ };
96
+ } catch {
97
+ try {
98
+ const stat = fs.statSync(dir);
99
+ if (now - stat.mtimeMs > REFRESH_LOCK_STALE_MS) {
100
+ fs.rmSync(dir, { recursive: true, force: true });
101
+ return acquireRefreshLock(now);
102
+ }
103
+ } catch {
104
+ // ignore lock inspection failures
105
+ }
106
+ return null;
107
+ }
108
+ }
109
+
52
110
  export function maybeWarnNewVersion(): void {
53
111
  if (isOptedOut()) return;
54
112
  const entry = readCache();
@@ -68,7 +126,14 @@ export function maybeWarnNewVersion(): void {
68
126
 
69
127
  export async function refreshVersionCache(): Promise<void> {
70
128
  if (isOptedOut()) return;
129
+ if (isKnownNoninteractiveStatusProbe()) return;
130
+ const existing = readCache();
131
+ if (existing && freshEnough(existing)) return;
132
+ const releaseLock = acquireRefreshLock();
133
+ if (!releaseLock) return;
71
134
  try {
135
+ const lockedExisting = readCache();
136
+ if (lockedExisting && freshEnough(lockedExisting)) return;
72
137
  const res = await fetch(REGISTRY_URL, {
73
138
  headers: { Accept: "application/json" },
74
139
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
@@ -79,5 +144,12 @@ export async function refreshVersionCache(): Promise<void> {
79
144
  writeCache({ latest: body.version, fetchedAt: Date.now() });
80
145
  } catch {
81
146
  // best-effort; offline / registry down / timeout — silent
147
+ } finally {
148
+ releaseLock();
82
149
  }
83
150
  }
151
+
152
+ export const __test__ = {
153
+ CACHE_TTL_MS,
154
+ isKnownNoninteractiveStatusProbe,
155
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Tenant isolation regression suite (US-011) — CLI + control-plane client side.
3
+ *
4
+ * API matrix lives primarily in hq-pro; this suite asserts CLI path denial and
5
+ * secret-ref namespacing expectations.
6
+ */
7
+
8
+ import fs from "node:fs";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+
12
+ import { afterEach, describe, expect, it, vi } from "vitest";
13
+
14
+ import { ControlPlaneDbClient } from "../../src/lib/db/control-plane.js";
15
+ import { openLocalDb } from "../../src/lib/db/local.js";
16
+ import { resolveLocalDbPath } from "../../src/lib/db/paths.js";
17
+ import { runLocalSql } from "../../src/lib/db/sql.js";
18
+
19
+ const temps: string[] = [];
20
+
21
+ afterEach(() => {
22
+ while (temps.length) {
23
+ const d = temps.pop();
24
+ if (d) fs.rmSync(d, { recursive: true, force: true });
25
+ }
26
+ });
27
+
28
+ describe("tenant isolation (local + remote client)", () => {
29
+ it("company A cannot open company B local path by override", () => {
30
+ const home = fs.mkdtempSync(path.join(os.tmpdir(), "hq-db-iso-"));
31
+ temps.push(home);
32
+ openLocalDb("alpha", { home }).close();
33
+ openLocalDb("beta", { home }).close();
34
+ const betaPath = resolveLocalDbPath("beta", { home });
35
+
36
+ expect(() =>
37
+ runLocalSql({
38
+ company: "alpha",
39
+ home,
40
+ sql: "SELECT 1",
41
+ dbPathOverride: betaPath,
42
+ }),
43
+ ).toThrow(/denied/i);
44
+ });
45
+
46
+ it("Y cannot provision X via control plane (403)", async () => {
47
+ const fetchImpl = vi.fn(async () => {
48
+ return new Response(JSON.stringify({ error: "Forbidden" }), {
49
+ status: 403,
50
+ });
51
+ }) as unknown as typeof fetch;
52
+
53
+ const client = new ControlPlaneDbClient({
54
+ baseUrl: "https://api.example",
55
+ getAccessToken: async () => "tok-y",
56
+ fetchImpl,
57
+ });
58
+
59
+ await expect(
60
+ client.provision({ companyUid: "cmp_x", companySlug: "x" }),
61
+ ).rejects.toMatchObject({ status: 403 });
62
+ });
63
+
64
+ it("secret refs are company-namespaced", async () => {
65
+ const fetchImpl = vi.fn(async () => {
66
+ return new Response(
67
+ JSON.stringify({
68
+ ok: true,
69
+ companyUid: "cmp_x",
70
+ companySlug: "x",
71
+ engineId: "aurora-dsql",
72
+ resourceArn: "arn:aws:dsql:us-east-1:1:cluster/x",
73
+ region: "us-east-1",
74
+ status: "ready",
75
+ secretRef: "ssm:/hq/cmp_x/secrets/DB/REMOTE/X",
76
+ idempotent: false,
77
+ }),
78
+ { status: 200 },
79
+ );
80
+ }) as unknown as typeof fetch;
81
+
82
+ const client = new ControlPlaneDbClient({
83
+ baseUrl: "https://api.example",
84
+ getAccessToken: async () => "tok",
85
+ fetchImpl,
86
+ });
87
+ const result = await client.provision({
88
+ companyUid: "cmp_x",
89
+ companySlug: "x",
90
+ });
91
+ expect(result.secretRef).toContain("cmp_x");
92
+ expect(result.secretRef).not.toContain("cmp_y");
93
+ });
94
+ });
@@ -0,0 +1,85 @@
1
+ /**
2
+ * C1 local `hq db` harness (vault-databases US-006).
3
+ *
4
+ * Offline only: temp HOME + temp HQ root, zero network.
5
+ * Covers path resolution, status auto-create, sql select, migrate apply/idempotent.
6
+ */
7
+
8
+ import fs from "node:fs";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+
12
+ import { afterEach, describe, expect, it } from "vitest";
13
+
14
+ import { ensureAndStatusLocalDb } from "../../src/lib/db/local.js";
15
+ import { migrateLocalDb } from "../../src/lib/db/migrate.js";
16
+ import { resolveLocalDbPath } from "../../src/lib/db/paths.js";
17
+ import { runLocalSql } from "../../src/lib/db/sql.js";
18
+
19
+ const temps: string[] = [];
20
+
21
+ function fixture(): { home: string; hqRoot: string } {
22
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "hq-db-harness-"));
23
+ temps.push(root);
24
+ const home = path.join(root, "home");
25
+ const hqRoot = path.join(root, "hq");
26
+ fs.mkdirSync(home, { recursive: true });
27
+ fs.mkdirSync(hqRoot, { recursive: true });
28
+ return { home, hqRoot };
29
+ }
30
+
31
+ afterEach(() => {
32
+ while (temps.length > 0) {
33
+ const d = temps.pop();
34
+ if (d) fs.rmSync(d, { recursive: true, force: true });
35
+ }
36
+ });
37
+
38
+ describe("hq db C1 harness (offline)", () => {
39
+ it("path resolution for two company slugs under documented root", () => {
40
+ const { home } = fixture();
41
+ const a = resolveLocalDbPath("indigo", { home });
42
+ const b = resolveLocalDbPath("acme", { home });
43
+ expect(a).toContain(path.join(".hq", "db", "indigo", "vault.db"));
44
+ expect(b).toContain(path.join(".hq", "db", "acme", "vault.db"));
45
+ expect(a).not.toBe(b);
46
+ });
47
+
48
+ it("status auto-creates WAL SQLite and reopen is stable", () => {
49
+ const { home } = fixture();
50
+ const first = ensureAndStatusLocalDb("fixture", { home });
51
+ expect(first.healthy).toBe(true);
52
+ expect(first.journalMode?.toLowerCase()).toBe("wal");
53
+ expect(fs.existsSync(first.path)).toBe(true);
54
+
55
+ const size1 = fs.statSync(first.path).size;
56
+ const second = ensureAndStatusLocalDb("fixture", { home });
57
+ expect(second.healthy).toBe(true);
58
+ expect(fs.statSync(second.path).size).toBeGreaterThanOrEqual(size1);
59
+ });
60
+
61
+ it("sql select after migrate smoke table", () => {
62
+ const { home, hqRoot } = fixture();
63
+ const migDir = path.join(hqRoot, "companies", "fixture", "db", "migrations");
64
+ fs.mkdirSync(migDir, { recursive: true });
65
+ fs.writeFileSync(
66
+ path.join(migDir, "001_smoke.sql"),
67
+ "CREATE TABLE smoke (id INTEGER PRIMARY KEY, v TEXT);\nINSERT INTO smoke (v) VALUES ('ok');\n",
68
+ "utf8",
69
+ );
70
+
71
+ const mig = migrateLocalDb({ company: "fixture", home, hqRoot });
72
+ expect(mig.applied).toContain("001_smoke.sql");
73
+
74
+ const rows = runLocalSql({
75
+ company: "fixture",
76
+ home,
77
+ sql: "SELECT v FROM smoke",
78
+ });
79
+ expect(rows.rows[0]).toMatchObject({ v: "ok" });
80
+
81
+ const again = migrateLocalDb({ company: "fixture", home, hqRoot });
82
+ expect(again.applied).toEqual([]);
83
+ expect(again.skipped.length).toBeGreaterThan(0);
84
+ });
85
+ });