@indigoai-us/hq-cli 5.121.1 → 5.121.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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [5.121.2] — 2026-09-18
6
+
7
+ ### Added
8
+
9
+ - `hq cloud retire company <slug>` soft-tombstones a cloud company (owner
10
+ only, confirm unless `--yes`) so `hq cloud demote company` can run without
11
+ `--propagate-deletes`. Vault files stay; local folders are not touched.
12
+
5
13
  ## [5.121.1] — 2026-09-18
6
14
 
7
15
  ### Fixed
@@ -722,6 +722,37 @@ export declare const COMMAND_CATALOG: readonly [{
722
722
  }];
723
723
  readonly subcommands: readonly [];
724
724
  }];
725
+ }, {
726
+ readonly name: "retire";
727
+ readonly description: "Soft-tombstone a cloud-backed company (owner only)";
728
+ readonly aliases: readonly [];
729
+ readonly hidden: false;
730
+ readonly usage: "[options] [command]";
731
+ readonly arguments: readonly [];
732
+ readonly options: readonly [];
733
+ readonly subcommands: readonly [{
734
+ readonly name: "company";
735
+ readonly description: "Soft-tombstone a cloud company so `hq cloud demote company` can run. Does not delete vault files or local folders. Prompts unless --yes.";
736
+ readonly aliases: readonly [];
737
+ readonly hidden: false;
738
+ readonly usage: "[options] <slug>";
739
+ readonly arguments: readonly [{
740
+ readonly name: "slug";
741
+ readonly required: true;
742
+ readonly variadic: false;
743
+ }];
744
+ readonly options: readonly [{
745
+ readonly flags: "--hq-root <path>";
746
+ readonly description: "Local HQ tree root (default: <runtime-path>)";
747
+ }, {
748
+ readonly flags: "--vault-api-url <url>";
749
+ readonly description: "Vault API URL (default: https://hqapi.hq.computer)";
750
+ }, {
751
+ readonly flags: "-y, --yes";
752
+ readonly description: "Skip the confirmation prompt (for scripts)";
753
+ }];
754
+ readonly subcommands: readonly [];
755
+ }];
725
756
  }, {
726
757
  readonly name: "demote";
727
758
  readonly description: "Demote a cloud-backed entity back to local-only";
@@ -732,7 +763,7 @@ export declare const COMMAND_CATALOG: readonly [{
732
763
  readonly options: readonly [];
733
764
  readonly subcommands: readonly [{
734
765
  readonly name: "company";
735
- readonly description: "Demote a cloud-backed company to local-only after the cloud entity has been soft-tombstoned in hq-console. Removes .hq/config.json, flips company.yaml `cloud: false`, and strips the manifest cloud refs.";
766
+ readonly description: "Demote a cloud-backed company to local-only after the cloud entity has been soft-tombstoned (`hq cloud retire company` or hq-console). Removes .hq/config.json, flips company.yaml `cloud: false`, and strips the manifest cloud refs.";
736
767
  readonly aliases: readonly [];
737
768
  readonly hidden: false;
738
769
  readonly usage: "[options] <slug>";
@@ -924,6 +924,46 @@ export const COMMAND_CATALOG = [
924
924
  }
925
925
  ]
926
926
  },
927
+ {
928
+ "name": "retire",
929
+ "description": "Soft-tombstone a cloud-backed company (owner only)",
930
+ "aliases": [],
931
+ "hidden": false,
932
+ "usage": "[options] [command]",
933
+ "arguments": [],
934
+ "options": [],
935
+ "subcommands": [
936
+ {
937
+ "name": "company",
938
+ "description": "Soft-tombstone a cloud company so `hq cloud demote company` can run. Does not delete vault files or local folders. Prompts unless --yes.",
939
+ "aliases": [],
940
+ "hidden": false,
941
+ "usage": "[options] <slug>",
942
+ "arguments": [
943
+ {
944
+ "name": "slug",
945
+ "required": true,
946
+ "variadic": false
947
+ }
948
+ ],
949
+ "options": [
950
+ {
951
+ "flags": "--hq-root <path>",
952
+ "description": "Local HQ tree root (default: <runtime-path>)"
953
+ },
954
+ {
955
+ "flags": "--vault-api-url <url>",
956
+ "description": "Vault API URL (default: https://hqapi.hq.computer)"
957
+ },
958
+ {
959
+ "flags": "-y, --yes",
960
+ "description": "Skip the confirmation prompt (for scripts)"
961
+ }
962
+ ],
963
+ "subcommands": []
964
+ }
965
+ ]
966
+ },
927
967
  {
928
968
  "name": "demote",
929
969
  "description": "Demote a cloud-backed entity back to local-only",
@@ -935,7 +975,7 @@ export const COMMAND_CATALOG = [
935
975
  "subcommands": [
936
976
  {
937
977
  "name": "company",
938
- "description": "Demote a cloud-backed company to local-only after the cloud entity has been soft-tombstoned in hq-console. Removes .hq/config.json, flips company.yaml `cloud: false`, and strips the manifest cloud refs.",
978
+ "description": "Demote a cloud-backed company to local-only after the cloud entity has been soft-tombstoned (`hq cloud retire company` or hq-console). Removes .hq/config.json, flips company.yaml `cloud: false`, and strips the manifest cloud refs.",
939
979
  "aliases": [],
940
980
  "hidden": false,
941
981
  "usage": "[options] <slug>",
@@ -141,6 +141,12 @@ export declare const REGISTRATION_PLAN: readonly [{
141
141
  readonly parent: "cloud";
142
142
  readonly module: "./commands/cloud-provision.js";
143
143
  readonly exportName: "registerCloudProvisionCommands";
144
+ }, {
145
+ readonly type: "registrar";
146
+ readonly root: "cloud";
147
+ readonly parent: "cloud";
148
+ readonly module: "./commands/cloud-retire.js";
149
+ readonly exportName: "registerCloudRetireCommands";
144
150
  }, {
145
151
  readonly type: "registrar";
146
152
  readonly root: "cloud";
@@ -26,6 +26,7 @@ export const REGISTRATION_PLAN = [
26
26
  { type: "registrar", root: "sync", parent: "sync", module: "./commands/sync-manifest.js", exportName: "registerSyncManifestCommand" },
27
27
  { type: "group", root: "cloud", id: "cloud", name: "cloud", description: "Cloud commands — provision entities and manage cloud-backed companies" },
28
28
  { type: "registrar", root: "cloud", parent: "cloud", module: "./commands/cloud-provision.js", exportName: "registerCloudProvisionCommands" },
29
+ { type: "registrar", root: "cloud", parent: "cloud", module: "./commands/cloud-retire.js", exportName: "registerCloudRetireCommands" },
29
30
  { type: "registrar", root: "cloud", parent: "cloud", module: "./commands/cloud-demote.js", exportName: "registerCloudDemoteCommands" },
30
31
  { type: "registrar", root: "team-sync", parent: "program", module: "./commands/team-sync.js", exportName: "registerTeamSyncCommand" },
31
32
  { type: "registrar", root: "login", parent: "program", module: "./commands/login.js", exportName: "registerLoginCommand" },
@@ -160,8 +160,9 @@ export function registerCloudDemoteCommands(program) {
160
160
  demoteCmd
161
161
  .command("company")
162
162
  .description("Demote a cloud-backed company to local-only after the cloud entity " +
163
- "has been soft-tombstoned in hq-console. Removes .hq/config.json, " +
164
- "flips company.yaml `cloud: false`, and strips the manifest cloud refs.")
163
+ "has been soft-tombstoned (`hq cloud retire company` or hq-console). " +
164
+ "Removes .hq/config.json, flips company.yaml `cloud: false`, and " +
165
+ "strips the manifest cloud refs.")
165
166
  .argument("<slug>", "Company slug")
166
167
  .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
167
168
  .option("--vault-api-url <url>", `Vault API URL (default: ${DEFAULT_VAULT_API_URL})`, DEFAULT_VAULT_API_URL)
@@ -38,6 +38,8 @@ export interface VaultEntity {
38
38
  kmsKeyId?: string | null;
39
39
  status?: string;
40
40
  ownerUid?: string;
41
+ /** Set by hq-pro after a soft tombstone. Absent on live companies. */
42
+ deleted?: boolean;
41
43
  }
42
44
  /** Per-company `.hq/config.json` schema (matches AppBar `provision.rs::CompanyConfig`). */
43
45
  export interface CompanyConfig {
@@ -142,6 +144,15 @@ export interface VaultClient {
142
144
  name: string;
143
145
  ownerUid?: string;
144
146
  }): Promise<VaultEntity>;
147
+ /**
148
+ * Soft-tombstone a company (`DELETE /entity/{uid}`). Owner-only. Leaves the
149
+ * row, S3 bucket, KMS key, and memberships intact. Idempotent on an already
150
+ * deleted company. Used by `hq cloud retire company`.
151
+ */
152
+ softDeleteCompany(uid: string): Promise<{
153
+ deleted: true;
154
+ uid: string;
155
+ }>;
145
156
  }
146
157
  /** Custom error class so the CLI runner can map to exit codes. */
147
158
  export declare class ProvisionError extends Error {
@@ -364,6 +364,31 @@ export function createDefaultVaultClient(apiUrl, accessToken) {
364
364
  }
365
365
  return data.entity;
366
366
  },
367
+ async softDeleteCompany(uid) {
368
+ const url = `${apiUrl.replace(/\/$/, "")}/entity/${encodeURIComponent(uid)}`;
369
+ const res = await fetch(url, { method: "DELETE", headers });
370
+ if (res.status === 403) {
371
+ const body = await safeBody(res);
372
+ throw new ProvisionError(2, `Refusing to retire '${uid}': only the company owner can soft-tombstone it. ${body}`);
373
+ }
374
+ if (res.status === 404) {
375
+ const body = await safeBody(res);
376
+ throw new ProvisionError(2, `Vault DELETE /entity/${uid} failed: not found — ${body}`);
377
+ }
378
+ if (res.status === 400) {
379
+ const body = await safeBody(res);
380
+ throw new ProvisionError(2, `Vault DELETE /entity/${uid} failed: ${res.status} ${res.statusText} — ${body}`);
381
+ }
382
+ if (!res.ok) {
383
+ const body = await safeBody(res);
384
+ throw new ProvisionError(1, `Vault DELETE /entity/${uid} failed: ${res.status} ${res.statusText} — ${body}`);
385
+ }
386
+ const data = (await res.json());
387
+ if (data.deleted !== true || typeof data.uid !== "string") {
388
+ throw new ProvisionError(1, `Vault DELETE /entity/${uid} returned ${res.status} with no deleted confirmation`);
389
+ }
390
+ return { deleted: true, uid: data.uid };
391
+ },
367
392
  };
368
393
  }
369
394
  async function safeBody(res) {
@@ -0,0 +1,51 @@
1
+ /**
2
+ * `hq cloud retire company <slug>` — soft-tombstone a cloud company so
3
+ * `hq cloud demote company` can run without `--propagate-deletes`.
4
+ *
5
+ * Console equivalent: Settings → Delete company (`DELETE /entity/{uid}`).
6
+ * Owner-only. The entity row, S3 bucket, KMS key, and memberships stay;
7
+ * `/membership/me` stops listing the company. Local files are not touched —
8
+ * demote is the follow-up that flips the local tree back to local-only.
9
+ *
10
+ * Confirm step is on by default (`[y/N]`). `--yes` skips it for scripts.
11
+ * A non-TTY stdin without `--yes` refuses rather than hanging.
12
+ *
13
+ * Exit codes (mirrors cloud-provision / cloud-demote):
14
+ * 0 — success, already tombstoned, or the operator cancelled.
15
+ * 1 — vault HTTP failure (auth/network/5xx).
16
+ * 2 — validation (bad slug, no entity, not owner, missing confirm).
17
+ */
18
+ import { Command } from "commander";
19
+ import { type VaultClient } from "./cloud-provision.js";
20
+ /** Final stdout JSON shape. */
21
+ export interface RetireResult {
22
+ ok: boolean;
23
+ company_slug: string;
24
+ cloud_uid: string | null;
25
+ /** True when hq-pro already had deleted=true (no DELETE sent). */
26
+ already_deleted: boolean;
27
+ cancelled?: boolean;
28
+ }
29
+ export interface RetireCompanyOptions {
30
+ slug: string;
31
+ hqRoot: string;
32
+ vaultApiUrl: string;
33
+ /** Skip the interactive confirmation prompt. */
34
+ yes?: boolean;
35
+ vaultClient?: VaultClient;
36
+ resolveAccessToken?: () => Promise<string>;
37
+ confirm?: (question: string) => Promise<boolean>;
38
+ stdinIsTTY?: boolean;
39
+ }
40
+ export declare function defaultConfirm(question: string): Promise<boolean>;
41
+ /**
42
+ * Read `companyUid` from `companies/<slug>/.hq/config.json` when the by-slug
43
+ * lookup misses (a tombstoned company is filtered from that route).
44
+ */
45
+ export declare function readLocalCompanyUid(hqRoot: string, slug: string): string | null;
46
+ export declare function retireCompany(options: RetireCompanyOptions): Promise<RetireResult>;
47
+ /**
48
+ * Register `retire company <slug>` under the `cloud` command group.
49
+ */
50
+ export declare function registerCloudRetireCommands(program: Command): void;
51
+ //# sourceMappingURL=cloud-retire.d.ts.map
@@ -0,0 +1,154 @@
1
+ /**
2
+ * `hq cloud retire company <slug>` — soft-tombstone a cloud company so
3
+ * `hq cloud demote company` can run without `--propagate-deletes`.
4
+ *
5
+ * Console equivalent: Settings → Delete company (`DELETE /entity/{uid}`).
6
+ * Owner-only. The entity row, S3 bucket, KMS key, and memberships stay;
7
+ * `/membership/me` stops listing the company. Local files are not touched —
8
+ * demote is the follow-up that flips the local tree back to local-only.
9
+ *
10
+ * Confirm step is on by default (`[y/N]`). `--yes` skips it for scripts.
11
+ * A non-TTY stdin without `--yes` refuses rather than hanging.
12
+ *
13
+ * Exit codes (mirrors cloud-provision / cloud-demote):
14
+ * 0 — success, already tombstoned, or the operator cancelled.
15
+ * 1 — vault HTTP failure (auth/network/5xx).
16
+ * 2 — validation (bad slug, no entity, not owner, missing confirm).
17
+ */
18
+ import * as fs from "node:fs";
19
+ import * as readline from "node:readline";
20
+ import chalk from "chalk";
21
+ import { ProvisionError, companyConfigPath, createDefaultVaultClient, validateSlug, } from "./cloud-provision.js";
22
+ import { DEFAULT_HQ_ROOT, DEFAULT_VAULT_API_URL, ensureCognitoToken, } from "../utils/cognito-session.js";
23
+ export function defaultConfirm(question) {
24
+ const rl = readline.createInterface({
25
+ input: process.stdin,
26
+ output: process.stdout,
27
+ });
28
+ return new Promise((resolve) => {
29
+ rl.question(`${question} [y/N] `, (answer) => {
30
+ rl.close();
31
+ resolve(/^y(es)?$/i.test(answer.trim()));
32
+ });
33
+ });
34
+ }
35
+ /**
36
+ * Read `companyUid` from `companies/<slug>/.hq/config.json` when the by-slug
37
+ * lookup misses (a tombstoned company is filtered from that route).
38
+ */
39
+ export function readLocalCompanyUid(hqRoot, slug) {
40
+ const cPath = companyConfigPath(hqRoot, slug);
41
+ if (!fs.existsSync(cPath))
42
+ return null;
43
+ try {
44
+ const parsed = JSON.parse(fs.readFileSync(cPath, "utf-8"));
45
+ return typeof parsed.companyUid === "string" && parsed.companyUid.length > 0
46
+ ? parsed.companyUid
47
+ : null;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ export async function retireCompany(options) {
54
+ validateSlug(options.slug);
55
+ const accessToken = options.resolveAccessToken
56
+ ? await options.resolveAccessToken()
57
+ : await ensureCognitoToken();
58
+ const client = options.vaultClient ??
59
+ createDefaultVaultClient(options.vaultApiUrl, accessToken);
60
+ let entity;
61
+ try {
62
+ entity = await client.findCompanyBySlug(options.slug);
63
+ }
64
+ catch (err) {
65
+ if (err instanceof ProvisionError)
66
+ throw err;
67
+ throw new ProvisionError(1, `Vault GET by-slug failed: ${err instanceof Error ? err.message : String(err)}`);
68
+ }
69
+ if (entity?.deleted === true) {
70
+ return {
71
+ ok: true,
72
+ company_slug: options.slug,
73
+ cloud_uid: entity.uid,
74
+ already_deleted: true,
75
+ };
76
+ }
77
+ const uid = entity?.uid ?? readLocalCompanyUid(options.hqRoot, options.slug);
78
+ if (!uid) {
79
+ throw new ProvisionError(2, `Refusing to retire '${options.slug}': no cloud entity found.`);
80
+ }
81
+ if (options.yes !== true) {
82
+ const tty = options.stdinIsTTY ?? process.stdin.isTTY === true;
83
+ if (!tty) {
84
+ throw new ProvisionError(2, `Refusing to retire '${options.slug}' without confirmation. Re-run with --yes.`);
85
+ }
86
+ const confirm = options.confirm ?? defaultConfirm;
87
+ const ok = await confirm(`Soft-tombstone cloud company '${options.slug}' (${uid})? ` +
88
+ `It will disappear from every member's picker. Vault files stay. ` +
89
+ `Then run: hq cloud demote company ${options.slug}`);
90
+ if (!ok) {
91
+ return {
92
+ ok: false,
93
+ company_slug: options.slug,
94
+ cloud_uid: uid,
95
+ already_deleted: false,
96
+ cancelled: true,
97
+ };
98
+ }
99
+ }
100
+ try {
101
+ await client.softDeleteCompany(uid);
102
+ }
103
+ catch (err) {
104
+ if (err instanceof ProvisionError)
105
+ throw err;
106
+ throw new ProvisionError(1, `Vault DELETE /entity/${uid} failed: ${err instanceof Error ? err.message : String(err)}`);
107
+ }
108
+ return {
109
+ ok: true,
110
+ company_slug: options.slug,
111
+ cloud_uid: uid,
112
+ already_deleted: false,
113
+ };
114
+ }
115
+ /**
116
+ * Register `retire company <slug>` under the `cloud` command group.
117
+ */
118
+ export function registerCloudRetireCommands(program) {
119
+ const retireCmd = program
120
+ .command("retire")
121
+ .description("Soft-tombstone a cloud-backed company (owner only)");
122
+ retireCmd
123
+ .command("company")
124
+ .description("Soft-tombstone a cloud company so `hq cloud demote company` can run. " +
125
+ "Does not delete vault files or local folders. Prompts unless --yes.")
126
+ .argument("<slug>", "Company slug")
127
+ .option("--hq-root <path>", `Local HQ tree root (default: ${DEFAULT_HQ_ROOT})`, DEFAULT_HQ_ROOT)
128
+ .option("--vault-api-url <url>", `Vault API URL (default: ${DEFAULT_VAULT_API_URL})`, DEFAULT_VAULT_API_URL)
129
+ .option("-y, --yes", "Skip the confirmation prompt (for scripts)")
130
+ .action(async (slug, options) => {
131
+ try {
132
+ const result = await retireCompany({
133
+ slug,
134
+ hqRoot: options.hqRoot,
135
+ vaultApiUrl: options.vaultApiUrl,
136
+ yes: options.yes,
137
+ });
138
+ process.stdout.write(JSON.stringify(result) + "\n");
139
+ if (result.cancelled) {
140
+ process.stderr.write(chalk.dim("[hq cloud retire] Cancelled — nothing was tombstoned.\n"));
141
+ }
142
+ process.exit(0);
143
+ }
144
+ catch (err) {
145
+ if (err instanceof ProvisionError) {
146
+ process.stderr.write(chalk.red(`[hq cloud retire] ${err.message}\n`));
147
+ process.exit(err.code);
148
+ }
149
+ process.stderr.write(chalk.red(`[hq cloud retire] Unexpected error: ${err instanceof Error ? err.message : String(err)}\n`));
150
+ process.exit(1);
151
+ }
152
+ });
153
+ }
154
+ //# sourceMappingURL=cloud-retire.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.121.1",
3
+ "version": "5.121.2",
4
4
  "description": "HQ by Indigo management CLI \u2014 modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {