@indigoai-us/hq-cli 5.61.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 (80) 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/secrets.d.ts +8 -0
  19. package/dist/commands/secrets.js +23 -8
  20. package/dist/commands/skill.d.ts +153 -0
  21. package/dist/commands/skill.js +593 -0
  22. package/dist/commands/workers.d.ts +48 -0
  23. package/dist/commands/workers.js +229 -0
  24. package/dist/lib/db/control-plane.d.ts +45 -0
  25. package/dist/lib/db/control-plane.js +81 -0
  26. package/dist/lib/db/local.d.ts +49 -0
  27. package/dist/lib/db/local.js +106 -0
  28. package/dist/lib/db/migrate.d.ts +41 -0
  29. package/dist/lib/db/migrate.js +104 -0
  30. package/dist/lib/db/paths.d.ts +56 -0
  31. package/dist/lib/db/paths.js +103 -0
  32. package/dist/lib/db/remote-engine.d.ts +58 -0
  33. package/dist/lib/db/remote-engine.js +90 -0
  34. package/dist/lib/db/remote-sql.d.ts +22 -0
  35. package/dist/lib/db/remote-sql.js +39 -0
  36. package/dist/lib/db/sql.d.ts +49 -0
  37. package/dist/lib/db/sql.js +132 -0
  38. package/dist/main.js +27 -2
  39. package/dist/utils/cognito-session.js +3 -3
  40. package/dist/utils/sandbox-runner-client.js +3 -3
  41. package/package.json +9 -1
  42. package/pnpm-workspace.yaml +2 -0
  43. package/src/commands/agents.test.ts +297 -0
  44. package/src/commands/agents.ts +561 -0
  45. package/src/commands/db-migrate.ts +55 -0
  46. package/src/commands/db-provision.ts +102 -0
  47. package/src/commands/db-sql.ts +124 -0
  48. package/src/commands/db-status.ts +100 -0
  49. package/src/commands/db.ts +26 -0
  50. package/src/commands/integrations.test.ts +284 -0
  51. package/src/commands/integrations.ts +438 -0
  52. package/src/commands/members.ts +2 -2
  53. package/src/commands/outposts.test.ts +177 -0
  54. package/src/commands/outposts.ts +338 -0
  55. package/src/commands/secrets.parse-destination.test.ts +38 -0
  56. package/src/commands/secrets.test.ts +24 -0
  57. package/src/commands/secrets.ts +30 -10
  58. package/src/commands/skill.test.ts +770 -0
  59. package/src/commands/skill.ts +796 -0
  60. package/src/commands/workers.test.ts +158 -0
  61. package/src/commands/workers.ts +298 -0
  62. package/src/lib/db/control-plane.test.ts +59 -0
  63. package/src/lib/db/control-plane.ts +113 -0
  64. package/src/lib/db/local.test.ts +81 -0
  65. package/src/lib/db/local.ts +148 -0
  66. package/src/lib/db/migrate.test.ts +133 -0
  67. package/src/lib/db/migrate.ts +137 -0
  68. package/src/lib/db/paths.test.ts +112 -0
  69. package/src/lib/db/paths.ts +128 -0
  70. package/src/lib/db/remote-engine.test.ts +44 -0
  71. package/src/lib/db/remote-engine.ts +148 -0
  72. package/src/lib/db/remote-sql.test.ts +32 -0
  73. package/src/lib/db/remote-sql.ts +62 -0
  74. package/src/lib/db/sql.test.ts +106 -0
  75. package/src/lib/db/sql.ts +192 -0
  76. package/src/main.ts +31 -0
  77. package/src/utils/cognito-session.ts +1 -1
  78. package/src/utils/sandbox-runner-client.ts +1 -1
  79. package/test/commands/db-tenant-isolation.test.ts +94 -0
  80. package/test/commands/db.test.ts +85 -0
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Local SQL execution helpers (vault-databases US-004).
3
+ *
4
+ * Company scope is path-bound via resolveLocalDbPath — callers must pass the
5
+ * resolved company slug from HQ session/flags. Path overrides that open
6
+ * another company's DB are denied unless an explicit dangerous flag is set.
7
+ */
8
+
9
+ import Database from "better-sqlite3";
10
+
11
+ import type { LocalDb } from "./local.js";
12
+ import { openLocalDb } from "./local.js";
13
+ import {
14
+ normalizeCompanySlugForLocalDb,
15
+ resolveLocalDbDir,
16
+ resolveLocalDbPath,
17
+ type LocalDbPathEnv,
18
+ } from "./paths.js";
19
+
20
+ export interface SqlRunOptions extends LocalDbPathEnv {
21
+ company: string;
22
+ sql: string;
23
+ /** Allow INSERT/UPDATE/DELETE/DDL. Default false (read-only). */
24
+ write?: boolean;
25
+ /**
26
+ * Dangerous: open an absolute DB path instead of the company canonical path.
27
+ * Denied unless allowCrossCompanyPath is true.
28
+ */
29
+ dbPathOverride?: string;
30
+ /** Explicit dangerous flag to open a non-canonical local DB path. Default false. */
31
+ allowCrossCompanyPath?: boolean;
32
+ /** Output format */
33
+ format?: "jsonl" | "table";
34
+ }
35
+
36
+ export interface SqlRunResult {
37
+ columns: string[];
38
+ rows: Record<string, unknown>[];
39
+ changes: number;
40
+ readonly: boolean;
41
+ }
42
+
43
+ const WRITE_PATTERN =
44
+ /^\s*(INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|ALTER|TRUNCATE|REINDEX|VACUUM|BEGIN|COMMIT|ROLLBACK)\b/i;
45
+
46
+ /** Always forbidden — opens other files/DBs and breaks company path isolation. */
47
+ const FORBIDDEN_SQL_PATTERN =
48
+ /\b(ATTACH|DETACH|LOAD_EXTENSION)\b/i;
49
+
50
+ export function stripSqlNoise(sql: string): string {
51
+ return sql
52
+ .replace(/^\s*--[^\n]*\n/gm, "")
53
+ .replace(/^\s*\/\*[\s\S]*?\*\//gm, "")
54
+ .trim();
55
+ }
56
+
57
+ export function isWriteSql(sql: string): boolean {
58
+ return WRITE_PATTERN.test(stripSqlNoise(sql));
59
+ }
60
+
61
+ /**
62
+ * Statements that must never run via `hq db sql`, even with --write.
63
+ * ATTACH/DETACH would let a company-scoped session open another company's
64
+ * vault.db by absolute path (category-1 isolation).
65
+ */
66
+ export function assertSqlAllowed(sql: string): void {
67
+ const stripped = stripSqlNoise(sql);
68
+ if (FORBIDDEN_SQL_PATTERN.test(stripped)) {
69
+ throw new Error(
70
+ "ATTACH/DETACH/LOAD_EXTENSION are not allowed via hq db sql (tenant isolation); use the company-canonical local path only",
71
+ );
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Resolve which file path to open, enforcing company isolation by default.
77
+ */
78
+ export function resolveSqlDbPath(opts: SqlRunOptions): string {
79
+ const company = normalizeCompanySlugForLocalDb(opts.company);
80
+ const canonical = resolveLocalDbPath(company, opts);
81
+
82
+ if (!opts.dbPathOverride) {
83
+ return canonical;
84
+ }
85
+
86
+ if (opts.dbPathOverride === canonical) {
87
+ return canonical;
88
+ }
89
+
90
+ if (!opts.allowCrossCompanyPath) {
91
+ throw new Error(
92
+ "cross-company or path override denied: refusing to open a non-canonical local DB path without --allow-cross-company-path (dangerous)",
93
+ );
94
+ }
95
+
96
+ return opts.dbPathOverride;
97
+ }
98
+
99
+ function openByPath(dbPath: string): LocalDb {
100
+ const db = new Database(dbPath);
101
+ db.pragma("journal_mode = WAL");
102
+ db.pragma("foreign_keys = ON");
103
+ return db;
104
+ }
105
+
106
+ /**
107
+ * Execute SQL against the company local vault DB.
108
+ */
109
+ export function runLocalSql(opts: SqlRunOptions): SqlRunResult {
110
+ const sql = opts.sql?.trim();
111
+ if (!sql) {
112
+ throw new Error("SQL statement is required");
113
+ }
114
+
115
+ assertSqlAllowed(sql);
116
+
117
+ const writeRequested = !!opts.write;
118
+ if (isWriteSql(sql) && !writeRequested) {
119
+ throw new Error(
120
+ "write/DDL statement blocked in read-only mode; pass --write to allow (prefer hq db migrate for schema changes)",
121
+ );
122
+ }
123
+
124
+ const dbPath = resolveSqlDbPath(opts);
125
+ const usingCanonical =
126
+ !opts.dbPathOverride || opts.dbPathOverride === resolveLocalDbPath(opts.company, opts);
127
+
128
+ const db: LocalDb = usingCanonical
129
+ ? openLocalDb(opts.company, opts)
130
+ : openByPath(dbPath);
131
+
132
+ try {
133
+ if (!writeRequested) {
134
+ db.pragma("query_only = ON");
135
+ }
136
+
137
+ const stmt = db.prepare(sql);
138
+ if (stmt.reader) {
139
+ const rows = stmt.all() as Record<string, unknown>[];
140
+ const columns =
141
+ rows.length > 0
142
+ ? Object.keys(rows[0] as object)
143
+ : stmt.columns().map((c) => c.name);
144
+ return {
145
+ columns,
146
+ rows,
147
+ changes: 0,
148
+ readonly: !writeRequested,
149
+ };
150
+ }
151
+
152
+ const info = stmt.run();
153
+ return {
154
+ columns: ["changes", "lastInsertRowid"],
155
+ rows: [
156
+ {
157
+ changes: info.changes,
158
+ lastInsertRowid: Number(info.lastInsertRowid),
159
+ },
160
+ ],
161
+ changes: info.changes,
162
+ readonly: !writeRequested,
163
+ };
164
+ } finally {
165
+ db.close();
166
+ }
167
+ }
168
+
169
+ export function formatSqlResult(
170
+ result: SqlRunResult,
171
+ format: "jsonl" | "table" = "jsonl",
172
+ ): string {
173
+ if (format === "jsonl") {
174
+ if (result.rows.length === 0) {
175
+ return JSON.stringify({ columns: result.columns, rows: 0 });
176
+ }
177
+ return result.rows.map((r) => JSON.stringify(r)).join("\n");
178
+ }
179
+
180
+ const cols = result.columns;
181
+ if (cols.length === 0) return "(no columns)";
182
+ const header = cols.join("\t");
183
+ const body = result.rows
184
+ .map((r) => cols.map((c) => String(r[c] ?? "")).join("\t"))
185
+ .join("\n");
186
+ return body ? `${header}\n${body}` : header;
187
+ }
188
+
189
+ /** Test helper: company dir for isolation assertions. */
190
+ export function companyLocalDbDir(company: string, env?: LocalDbPathEnv): string {
191
+ return resolveLocalDbDir(company, env);
192
+ }
package/src/main.ts CHANGED
@@ -35,9 +35,11 @@ import { registerApiKeysCommand } from "./commands/api-keys.js";
35
35
  import { registerSecretsCommand } from "./commands/secrets.js";
36
36
  import { registerRunCommand } from "./commands/run.js";
37
37
  import { registerGroupsCommand } from "./commands/groups.js";
38
+ import { registerWorkersCommand } from "./commands/workers.js";
38
39
  import { registerGroupGrantsCommand } from "./commands/group-grants.js";
39
40
  import { registerFilesCommand } from "./commands/files.js";
40
41
  import { registerFilesBrowseCommands } from "./commands/files-browse.js";
42
+ import { registerSkillCommand } from "./commands/skill.js";
41
43
  import { registerMembersCommand } from "./commands/members.js";
42
44
  import { registerPeopleCommand } from "./commands/people.js";
43
45
  import { registerDmCommand } from "./commands/dm.js";
@@ -46,11 +48,15 @@ import { registerFeedbackCommand } from "./commands/feedback.js";
46
48
  import { registerMeetingsCommand } from "./commands/meetings.js";
47
49
  import { registerSourcesCommand } from "./commands/sources.js";
48
50
  import { registerSignalsCommand } from "./commands/signals.js";
51
+ import { registerIntegrationsCommand } from "./commands/integrations.js";
49
52
  import { registerReindexCommand } from "./commands/reindex.js";
50
53
  import { registerRescueCommand } from "./commands/rescue.js";
51
54
  import { registerMcpCommand } from "./commands/mcp-status.js";
52
55
  import { registerCrmCommand } from "./commands/crm.js";
53
56
  import { registerCompanyCommand } from "./commands/company.js";
57
+ import { registerAgentsCommand } from "./commands/agents.js";
58
+ import { registerOutpostsCommand } from "./commands/outposts.js";
59
+ import { registerDbCommand } from "./commands/db.js";
54
60
  import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
55
61
  import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
56
62
  import { isEpipe } from "./utils/epipe.js";
@@ -160,6 +166,9 @@ registerAuthCommands(program);
160
166
  // Secrets management (subcommand group — hq secrets set|get|list|delete|exec|generate-link|cache)
161
167
  registerSecretsCommand(program);
162
168
 
169
+ // Vault databases (subcommand group — hq db status|sql|migrate|provision)
170
+ registerDbCommand(program);
171
+
163
172
  // API key management (subcommand group — hq api-keys create|list|revoke)
164
173
  registerApiKeysCommand(program);
165
174
 
@@ -169,6 +178,9 @@ registerRunCommand(program);
169
178
  // Groups management (subcommand group — hq groups create|delete|add|remove|list|members)
170
179
  registerGroupsCommand(program);
171
180
 
181
+ // Worker discovery + sharing (subcommand group — hq workers list|share)
182
+ registerWorkersCommand(program);
183
+
172
184
  // Cross-company group grants (subcommand group —
173
185
  // hq group-grants grant|revoke|outbound|inbound)
174
186
  registerGroupGrantsCommand(program);
@@ -179,6 +191,11 @@ registerGroupGrantsCommand(program);
179
191
  const filesCmd = registerFilesCommand(program);
180
192
  registerFilesBrowseCommands(filesCmd);
181
193
 
194
+ // Skill collaboration loop (subcommand group — hq skill suggest|list-suggestions|review).
195
+ // A thin terminal front-end over the SAME wired hq-pro skill suggestion + merge
196
+ // routes the MCP (US-007) and console merge (US-009) surfaces use — no forked logic.
197
+ registerSkillCommand(program);
198
+
182
199
  // Membership management (subcommand group — hq members invite|list|revoke)
183
200
  registerMembersCommand(program);
184
201
  // People directory (subcommand group — hq people list|search|resolve), reading
@@ -202,6 +219,10 @@ registerSourcesCommand(program);
202
219
  // Signals read surface (subcommand group — hq signals list|get|types|entities)
203
220
  registerSignalsCommand(program);
204
221
 
222
+ // Company-connected apps via the governed integration gateway
223
+ // (subcommand group — hq integrations list|tools|call|approve|reject)
224
+ registerIntegrationsCommand(program);
225
+
205
226
  // Skill/personal-overlay mirroring + workers-registry regen. Invoked by the
206
227
  // hq-core reindex hook shim (Stop / PostToolUse) and by sync()/rescue() after
207
228
  // they change on-disk sources. Keeps a `master-sync` alias for one release.
@@ -227,6 +248,16 @@ registerCrmCommand(program);
227
248
  // toggles for crmEnabled / ontologyEnabled via PUT /company-settings.
228
249
  registerCompanyCommand(program);
229
250
 
251
+ // Cloud agent management (subcommand group — `hq agents …`). Rename, reconfigure,
252
+ // start/stop, and tear down a company's fleet agents via the hq-pro /v1/agents
253
+ // control plane — the same routes the web console's agents panel calls.
254
+ registerAgentsCommand(program);
255
+
256
+ // Personal Outpost management (subcommand group — `hq outposts …`). List, inspect,
257
+ // enable Codex on, refresh login for, and destroy your EC2 boxes via the hq-pro
258
+ // /outpost/* control plane.
259
+ registerOutpostsCommand(program);
260
+
230
261
  export async function runCli(): Promise<void> {
231
262
  try {
232
263
  Sentry.addBreadcrumb({
@@ -57,7 +57,7 @@ export const DEFAULT_COGNITO: CognitoAuthConfig = {
57
57
  };
58
58
 
59
59
  export const DEFAULT_VAULT_API_URL =
60
- process.env.HQ_VAULT_API_URL ?? "https://hqapi.getindigo.ai";
60
+ process.env.HQ_VAULT_API_URL ?? "https://hqapi.hq.computer";
61
61
 
62
62
  /**
63
63
  * Resolve the HQ tree root for cloud-aware subcommands (`hq sync`, `hq onboard`,
@@ -38,7 +38,7 @@ export interface SandboxRunnerRetryOptions {
38
38
  maxDelayMs?: number;
39
39
  }
40
40
 
41
- const DEFAULT_SANDBOX_RUNNER_URL = "https://hqapi.getindigo.ai/sandbox";
41
+ const DEFAULT_SANDBOX_RUNNER_URL = "https://hqapi.hq.computer/sandbox";
42
42
  const RETRYABLE_STATUS_CODES = new Set([408, 425, 429, 500, 502, 503, 504]);
43
43
  const DEFAULT_RETRY_OPTIONS: Required<SandboxRunnerRetryOptions> = {
44
44
  maxAttempts: 8,
@@ -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
+ });