@indigoai-us/hq-cli 5.52.0 → 5.53.1
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 +30 -0
- package/dist/commands/company.d.ts +3 -0
- package/dist/commands/company.js +82 -0
- package/dist/commands/crm.d.ts +3 -0
- package/dist/commands/crm.js +162 -0
- package/dist/commands/files.d.ts +2 -0
- package/dist/commands/files.js +41 -13
- package/dist/commands/members.js +11 -2
- package/dist/index.js +11 -2
- package/package.json +1 -1
- package/src/commands/company.test.ts +177 -0
- package/src/commands/company.ts +132 -0
- package/src/commands/crm.test.ts +179 -0
- package/src/commands/crm.ts +236 -0
- package/src/commands/files-delete.test.ts +84 -0
- package/src/commands/files.ts +54 -13
- package/src/commands/members.test.ts +16 -1
- package/src/commands/members.ts +13 -0
- package/src/index.ts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.53.1]
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **`hq members promote` now clarifies admin access.** Promoting someone to
|
|
10
|
+
`owner` or `admin` prints that they automatically have full read and write
|
|
11
|
+
access to all of the company's context — there is no separate context grant to
|
|
12
|
+
give them (owners/admins resolve to full access via the server-side role
|
|
13
|
+
bypass) — and that what they actually see synced locally is governed by their
|
|
14
|
+
sync mode (`hq sync mode`), not by access. Member/guest promotions are
|
|
15
|
+
unchanged.
|
|
16
|
+
|
|
17
|
+
## [5.53.0]
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- **`hq crm entity upsert` — write a client record into the native CRM from the
|
|
22
|
+
terminal.** Posts to `POST /crm/entities`, which runs through the ontology
|
|
23
|
+
write-gate (tenant-isolated, `crmEnabled`-gated) and refreshes the company's
|
|
24
|
+
CRM projection. `--company`, `--type` (`contact|deal|contract|invoice|company`)
|
|
25
|
+
and `--name` are required in flag form; `--attio-id|--stripe-id|--pandadoc-id|
|
|
26
|
+
--neon-id` populate `external_ids`; `--json <file|->` passes a full entity or
|
|
27
|
+
array instead. Surfaces a `CRM_DISABLED` 403 with an enable hint. This is the
|
|
28
|
+
build-native path for companies with no external CRM.
|
|
29
|
+
- **`hq company settings set` — toggle company settings from the CLI.** Calls
|
|
30
|
+
`PUT /company-settings` with `--crm-enabled <true|false>` and/or
|
|
31
|
+
`--ontology-enabled <true|false>` (at least one required; owner-gated
|
|
32
|
+
server-side). `--crm-enabled true` is how `/crm-setup` activates the native
|
|
33
|
+
CRM for a company.
|
|
34
|
+
|
|
5
35
|
## [5.52.0]
|
|
6
36
|
|
|
7
37
|
### Added
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c0b7f9ed-8ddb-5422-8872-f85b6b406af6")}catch(e){}}();
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
5
|
+
import { vaultApiFetch, getEntityUid } from "../utils/vault-api.js";
|
|
6
|
+
/**
|
|
7
|
+
* Parse a `--flag <true|false>` string into a boolean, or throw. Commander
|
|
8
|
+
* passes the raw string; we validate strictly so a typo never silently writes
|
|
9
|
+
* the wrong value.
|
|
10
|
+
*/
|
|
11
|
+
function parseBoolFlag(value, flag) {
|
|
12
|
+
const v = value.trim().toLowerCase();
|
|
13
|
+
if (v === "true")
|
|
14
|
+
return true;
|
|
15
|
+
if (v === "false")
|
|
16
|
+
return false;
|
|
17
|
+
throw new Error(`${flag} must be 'true' or 'false' (got '${value}')`);
|
|
18
|
+
}
|
|
19
|
+
export function registerCompanyCommand(program) {
|
|
20
|
+
const company = program
|
|
21
|
+
.command("company")
|
|
22
|
+
.description("Company-level settings")
|
|
23
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)");
|
|
24
|
+
const settings = company
|
|
25
|
+
.command("settings")
|
|
26
|
+
.description("Per-company settings (owner-only)");
|
|
27
|
+
settings
|
|
28
|
+
.command("set")
|
|
29
|
+
.description("Set per-company settings (PUT /company-settings). Require at least one " +
|
|
30
|
+
"of --crm-enabled / --ontology-enabled.")
|
|
31
|
+
.option("--crm-enabled <true|false>", "Enable/disable the native CRM for this company")
|
|
32
|
+
.option("--ontology-enabled <true|false>", "Enable/disable the ontology gardener for this company")
|
|
33
|
+
.action(async (opts) => {
|
|
34
|
+
try {
|
|
35
|
+
const companySlug = company.opts().company;
|
|
36
|
+
if (!companySlug) {
|
|
37
|
+
console.error(chalk.red("Error: --company <slug> is required."));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
if (opts.crmEnabled === undefined && opts.ontologyEnabled === undefined) {
|
|
41
|
+
console.error(chalk.red("Error: provide at least one of --crm-enabled / --ontology-enabled."));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const body = {};
|
|
45
|
+
if (opts.crmEnabled !== undefined) {
|
|
46
|
+
body.crmEnabled = parseBoolFlag(opts.crmEnabled, "--crm-enabled");
|
|
47
|
+
}
|
|
48
|
+
if (opts.ontologyEnabled !== undefined) {
|
|
49
|
+
body.ontologyEnabled = parseBoolFlag(opts.ontologyEnabled, "--ontology-enabled");
|
|
50
|
+
}
|
|
51
|
+
const token = await ensureCognitoToken();
|
|
52
|
+
const companyUid = await getEntityUid(token, { companySlug });
|
|
53
|
+
body.companyUid = companyUid;
|
|
54
|
+
const res = await vaultApiFetch({
|
|
55
|
+
token,
|
|
56
|
+
path: "/company-settings",
|
|
57
|
+
method: "PUT",
|
|
58
|
+
body,
|
|
59
|
+
});
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
const errBody = (await res.json().catch(() => ({})));
|
|
62
|
+
if (res.status === 403) {
|
|
63
|
+
console.error(chalk.red("Requires owner role on this company to change its settings."));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
console.error(chalk.red(`Failed to update company settings: ${errBody.error ?? res.statusText}`));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
const applied = Object.entries(body)
|
|
70
|
+
.filter(([k]) => k !== "companyUid")
|
|
71
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
72
|
+
.join(", ");
|
|
73
|
+
console.log(chalk.green(`Company settings updated for ${companySlug}: ${applied}`));
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=company.js.map
|
|
82
|
+
//# debugId=c0b7f9ed-8ddb-5422-8872-f85b6b406af6
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="056ad757-a0da-5318-b97f-21ae6529f648")}catch(e){}}();
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
6
|
+
import { vaultApiFetch, getEntityUid } from "../utils/vault-api.js";
|
|
7
|
+
/**
|
|
8
|
+
* `hq crm entity upsert` — agent-facing controlled entity upsert
|
|
9
|
+
* (hq-native-crm US-002). Wraps `POST /crm/entities`, which fronts the ontology
|
|
10
|
+
* write gate so an authenticated company MEMBER (or higher) can create/update
|
|
11
|
+
* canonical CRM entities in their company's vault.
|
|
12
|
+
*
|
|
13
|
+
* Conventions mirror `secrets.ts`: ensureCognitoToken() → getEntityUid() →
|
|
14
|
+
* vaultApiFetch() → error-check → chalk output.
|
|
15
|
+
*/
|
|
16
|
+
/** The CRM/ontology entity types the upsert route accepts via `--type`. */
|
|
17
|
+
const CRM_ENTITY_TYPES = [
|
|
18
|
+
"contact",
|
|
19
|
+
"deal",
|
|
20
|
+
"contract",
|
|
21
|
+
"invoice",
|
|
22
|
+
"company",
|
|
23
|
+
];
|
|
24
|
+
/**
|
|
25
|
+
* Read the `--json <file|->` payload: a single entity object or an array of
|
|
26
|
+
* entities. `-` reads from stdin. Returns the entities array (always
|
|
27
|
+
* normalized to an array) or throws a descriptive Error.
|
|
28
|
+
*/
|
|
29
|
+
function readJsonEntities(source) {
|
|
30
|
+
const raw = source === "-"
|
|
31
|
+
? readFileSync(0, "utf8") // fd 0 = stdin
|
|
32
|
+
: readFileSync(source, "utf8");
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(raw);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
throw new Error(`--json payload is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
39
|
+
}
|
|
40
|
+
const entities = Array.isArray(parsed) ? parsed : [parsed];
|
|
41
|
+
if (entities.length === 0) {
|
|
42
|
+
throw new Error("--json payload contained no entities");
|
|
43
|
+
}
|
|
44
|
+
for (const e of entities) {
|
|
45
|
+
if (!e || typeof e !== "object") {
|
|
46
|
+
throw new Error("--json entities must be objects");
|
|
47
|
+
}
|
|
48
|
+
const obj = e;
|
|
49
|
+
if (typeof obj.type !== "string" || typeof obj.canonical_name !== "string") {
|
|
50
|
+
throw new Error("each --json entity needs a string `type` and `canonical_name`");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return entities;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Build a single entity from the flag form (`--type` + `--name` + optional
|
|
57
|
+
* external-id flags). The route fills the additive defaults (aliases/domain/
|
|
58
|
+
* relationships/confidence) when omitted, but we send a complete minimal shape.
|
|
59
|
+
*/
|
|
60
|
+
function buildEntityFromFlags(opts) {
|
|
61
|
+
const externalIds = {};
|
|
62
|
+
if (opts.attioId)
|
|
63
|
+
externalIds.attio = opts.attioId;
|
|
64
|
+
if (opts.stripeId)
|
|
65
|
+
externalIds.stripe = opts.stripeId;
|
|
66
|
+
if (opts.pandadocId)
|
|
67
|
+
externalIds.pandadoc = opts.pandadocId;
|
|
68
|
+
if (opts.neonId)
|
|
69
|
+
externalIds.neon = opts.neonId;
|
|
70
|
+
const entity = {
|
|
71
|
+
type: opts.type,
|
|
72
|
+
canonical_name: opts.name,
|
|
73
|
+
aliases: [],
|
|
74
|
+
confidence: 1,
|
|
75
|
+
domain: [],
|
|
76
|
+
relationships: [],
|
|
77
|
+
};
|
|
78
|
+
if (Object.keys(externalIds).length > 0) {
|
|
79
|
+
entity.external_ids = externalIds;
|
|
80
|
+
}
|
|
81
|
+
return entity;
|
|
82
|
+
}
|
|
83
|
+
export function registerCrmCommand(program) {
|
|
84
|
+
const crm = program
|
|
85
|
+
.command("crm")
|
|
86
|
+
.description("Native CRM — upsert canonical entities into a company vault")
|
|
87
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)");
|
|
88
|
+
const entity = crm
|
|
89
|
+
.command("entity")
|
|
90
|
+
.description("CRM entity operations");
|
|
91
|
+
entity
|
|
92
|
+
.command("upsert")
|
|
93
|
+
.description("Create or update a canonical CRM entity (POST /crm/entities). " +
|
|
94
|
+
"Use --type/--name (+ optional external-id flags) or --json <file|-> " +
|
|
95
|
+
"to pass a full entity/array.")
|
|
96
|
+
.option("--type <type>", `Entity type: ${CRM_ENTITY_TYPES.join(" | ")}`)
|
|
97
|
+
.option("--name <canonical_name>", "Canonical entity name")
|
|
98
|
+
.option("--attio-id <id>", "Attio record id → external_ids.attio")
|
|
99
|
+
.option("--stripe-id <id>", "Stripe object id → external_ids.stripe")
|
|
100
|
+
.option("--pandadoc-id <id>", "PandaDoc document id → external_ids.pandadoc")
|
|
101
|
+
.option("--neon-id <id>", "Neon row id → external_ids.neon")
|
|
102
|
+
.option("--json <file|->", "Path to a JSON file (or '-' for stdin) with a full entity object or array")
|
|
103
|
+
.action(async (opts) => {
|
|
104
|
+
try {
|
|
105
|
+
const companySlug = crm.opts().company;
|
|
106
|
+
if (!companySlug) {
|
|
107
|
+
console.error(chalk.red("Error: --company <slug> is required."));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
// Resolve the entity payload: --json is the alternative to flags.
|
|
111
|
+
let entities;
|
|
112
|
+
if (opts.json) {
|
|
113
|
+
entities = readJsonEntities(opts.json);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
if (!opts.type) {
|
|
117
|
+
console.error(chalk.red(`Error: --type <${CRM_ENTITY_TYPES.join("|")}> is required (or pass --json).`));
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
if (!CRM_ENTITY_TYPES.includes(opts.type)) {
|
|
121
|
+
console.error(chalk.red(`Error: --type must be one of ${CRM_ENTITY_TYPES.join(", ")}.`));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
if (!opts.name) {
|
|
125
|
+
console.error(chalk.red("Error: --name <canonical_name> is required (or pass --json)."));
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
entities = [buildEntityFromFlags(opts)];
|
|
129
|
+
}
|
|
130
|
+
const token = await ensureCognitoToken();
|
|
131
|
+
const companyUid = await getEntityUid(token, { companySlug });
|
|
132
|
+
const res = await vaultApiFetch({
|
|
133
|
+
token,
|
|
134
|
+
path: "/crm/entities",
|
|
135
|
+
method: "POST",
|
|
136
|
+
body: { companyUid, entities },
|
|
137
|
+
});
|
|
138
|
+
if (!res.ok) {
|
|
139
|
+
const body = (await res.json().catch(() => ({})));
|
|
140
|
+
// CrmDisabledError surfaces as a 403 with a helpful message.
|
|
141
|
+
if (res.status === 403 && body.code === "CRM_DISABLED") {
|
|
142
|
+
console.error(chalk.red("CRM is not enabled for this company. Enable it first: " +
|
|
143
|
+
`hq company settings set --company ${companySlug} --crm-enabled true`));
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
console.error(chalk.red(`Failed to upsert entities: ${body.error ?? res.statusText}`));
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
const result = (await res.json().catch(() => ({})));
|
|
150
|
+
console.log(chalk.green(`Upsert ${result.status ?? "ok"}: ` +
|
|
151
|
+
`${result.created ?? 0} created, ` +
|
|
152
|
+
`${result.updated ?? 0} updated, ` +
|
|
153
|
+
`${result.skipped ?? 0} skipped.`));
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=crm.js.map
|
|
162
|
+
//# debugId=056ad757-a0da-5318-b97f-21ae6529f648
|
package/dist/commands/files.d.ts
CHANGED
package/dist/commands/files.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="af29c08f-dbeb-5406-8deb-fdc263354b4a")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import open from "open";
|
|
5
5
|
import * as readline from "node:readline";
|
|
@@ -356,16 +356,24 @@ export function registerFilesCommand(program) {
|
|
|
356
356
|
});
|
|
357
357
|
files
|
|
358
358
|
.command("delete <prefix>")
|
|
359
|
-
.description("Delete vault objects under a prefix (bounded + scoped). Always previews the exact count first; prompts for confirmation unless --yes.")
|
|
359
|
+
.description("Delete vault objects under a prefix (bounded + scoped). Always previews the exact count first; prompts for confirmation unless --yes. Use --personal to target your own personal vault instead of a company.")
|
|
360
360
|
.option("--dry-run", "List what WOULD be deleted without deleting anything")
|
|
361
|
+
.option("--personal", "Target your own personal vault instead of a company vault (mutually exclusive with --company)")
|
|
361
362
|
.option("-y, --yes", "Skip the confirmation prompt (for scripts)")
|
|
362
363
|
.action(async (prefix, opts) => {
|
|
363
364
|
try {
|
|
365
|
+
const companySlug = files.opts().company;
|
|
366
|
+
const personal = opts.personal === true;
|
|
367
|
+
if (personal && companySlug) {
|
|
368
|
+
console.error(chalk.red("Pass either --personal or --company, not both."));
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
364
371
|
await runFilesDelete({
|
|
365
372
|
prefix,
|
|
366
373
|
dryRun: opts.dryRun === true,
|
|
367
374
|
yes: opts.yes === true,
|
|
368
|
-
|
|
375
|
+
personal,
|
|
376
|
+
companySlug,
|
|
369
377
|
});
|
|
370
378
|
}
|
|
371
379
|
catch (err) {
|
|
@@ -563,17 +571,28 @@ function realConfirm(message) {
|
|
|
563
571
|
/**
|
|
564
572
|
* POST /v1/files/delete. Throws FilesDeleteHttpError on any non-2xx so the one
|
|
565
573
|
* caller renders a single consistent error path.
|
|
574
|
+
*
|
|
575
|
+
* Scope is EITHER a company vault (`companyUid` set) OR the caller's personal
|
|
576
|
+
* vault (`personal: true`). For the personal case the server resolves the target
|
|
577
|
+
* person + bucket from the authenticated caller — we send NO uid, just the
|
|
578
|
+
* `personal` flag — so there is nothing for the client to get wrong or spoof.
|
|
566
579
|
*/
|
|
567
580
|
async function callDeleteEndpoint(params) {
|
|
568
581
|
const res = await vaultApiFetch({
|
|
569
582
|
token: params.token,
|
|
570
583
|
path: "/v1/files/delete",
|
|
571
584
|
method: "POST",
|
|
572
|
-
body:
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
585
|
+
body: params.personal
|
|
586
|
+
? {
|
|
587
|
+
personal: true,
|
|
588
|
+
prefix: params.prefix,
|
|
589
|
+
dryRun: params.dryRun,
|
|
590
|
+
}
|
|
591
|
+
: {
|
|
592
|
+
company: params.companyUid,
|
|
593
|
+
prefix: params.prefix,
|
|
594
|
+
dryRun: params.dryRun,
|
|
595
|
+
},
|
|
577
596
|
});
|
|
578
597
|
if (!res.ok) {
|
|
579
598
|
const body = (await res.json().catch(() => ({})));
|
|
@@ -642,14 +661,21 @@ export async function runFilesDelete(params, deps = {}) {
|
|
|
642
661
|
process.exit(1);
|
|
643
662
|
}
|
|
644
663
|
const token = await ensureCognitoToken();
|
|
645
|
-
|
|
664
|
+
// Personal scope resolves the vault server-side from the caller's identity —
|
|
665
|
+
// no company to look up. Company scope resolves the companyUid as before.
|
|
666
|
+
const companyUid = params.personal
|
|
667
|
+
? undefined
|
|
668
|
+
: await getCompanyUid(token, params.companySlug);
|
|
669
|
+
const scopeArgs = params.personal
|
|
670
|
+
? { personal: true }
|
|
671
|
+
: { companyUid };
|
|
646
672
|
// 1. Always preview first — this is how we print the EXACT key count before
|
|
647
673
|
// deleting anything (and the whole behavior of --dry-run).
|
|
648
674
|
let preview;
|
|
649
675
|
try {
|
|
650
676
|
preview = await callDeleteEndpoint({
|
|
651
677
|
token,
|
|
652
|
-
|
|
678
|
+
...scopeArgs,
|
|
653
679
|
prefix: normalized,
|
|
654
680
|
dryRun: true,
|
|
655
681
|
});
|
|
@@ -682,7 +708,9 @@ export async function runFilesDelete(params, deps = {}) {
|
|
|
682
708
|
console.log(chalk.dim(` (${preview.skipped} more under this prefix you can't delete will be left untouched)`));
|
|
683
709
|
}
|
|
684
710
|
if (!params.yes) {
|
|
685
|
-
const ok = await confirm(
|
|
711
|
+
const ok = await confirm(params.personal
|
|
712
|
+
? `Delete ${preview.matched} ${noun}? This removes them from your personal vault.`
|
|
713
|
+
: `Delete ${preview.matched} ${noun}? This removes them from the shared vault for everyone.`);
|
|
686
714
|
if (!ok) {
|
|
687
715
|
console.log(chalk.dim("Aborted — nothing was deleted."));
|
|
688
716
|
return;
|
|
@@ -693,7 +721,7 @@ export async function runFilesDelete(params, deps = {}) {
|
|
|
693
721
|
try {
|
|
694
722
|
result = await callDeleteEndpoint({
|
|
695
723
|
token,
|
|
696
|
-
|
|
724
|
+
...scopeArgs,
|
|
697
725
|
prefix: normalized,
|
|
698
726
|
dryRun: false,
|
|
699
727
|
});
|
|
@@ -712,4 +740,4 @@ export async function runFilesDelete(params, deps = {}) {
|
|
|
712
740
|
}
|
|
713
741
|
}
|
|
714
742
|
//# sourceMappingURL=files.js.map
|
|
715
|
-
//# debugId=
|
|
743
|
+
//# debugId=af29c08f-dbeb-5406-8deb-fdc263354b4a
|
package/dist/commands/members.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="c844265d-9140-55ac-8fc7-c0b2a049a0af")}catch(e){}}();
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
5
5
|
import { vaultApiFetch, getCompanyUid } from "../utils/vault-api.js";
|
|
@@ -316,6 +316,15 @@ export function registerMembersCommand(program) {
|
|
|
316
316
|
const membershipKey = resolveRevokeTargetToMembershipKey(target, companyUid);
|
|
317
317
|
await changeMemberRole(token, companyUid, membershipKey, role);
|
|
318
318
|
console.log(chalk.green(`Updated role for '${target}' to ${role}`));
|
|
319
|
+
if (role === "owner" || role === "admin") {
|
|
320
|
+
// Clarify the common confusion: owners/admins already get full
|
|
321
|
+
// access to ALL company context automatically via the role bypass
|
|
322
|
+
// (hq-pro resolveEffectivePermission) — there is no separate context
|
|
323
|
+
// grant to give them. What they SEE locally is governed by their sync
|
|
324
|
+
// mode, not by access.
|
|
325
|
+
console.log(chalk.dim(`${role === "owner" ? "Owners" : "Admins"} automatically have full read and write access to all of this company's context — no separate context grant is needed.\n` +
|
|
326
|
+
`What they actually see synced to their machine is governed by their sync mode (\`hq sync mode\`), not by access.`));
|
|
327
|
+
}
|
|
319
328
|
}
|
|
320
329
|
catch (err) {
|
|
321
330
|
if (err instanceof InviteHttpError) {
|
|
@@ -555,4 +564,4 @@ export function registerMembersCommand(program) {
|
|
|
555
564
|
});
|
|
556
565
|
}
|
|
557
566
|
//# sourceMappingURL=members.js.map
|
|
558
|
-
//# debugId=
|
|
567
|
+
//# debugId=c844265d-9140-55ac-8fc7-c0b2a049a0af
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// MUST be first: guard the Node version before any dependency that needs a
|
|
6
6
|
// Node 20+ API (e.g. util.styleText) or a newer native ABI is evaluated.
|
|
7
7
|
|
|
8
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
8
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="6ad01242-c165-5be2-bb31-e929c0b38f5e")}catch(e){}}();
|
|
9
9
|
import "./node-preflight.js";
|
|
10
10
|
import { Command } from "commander";
|
|
11
11
|
import { initSentry, Sentry } from "./sentry.js";
|
|
@@ -48,6 +48,8 @@ import { registerSignalsCommand } from "./commands/signals.js";
|
|
|
48
48
|
import { registerReindexCommand } from "./commands/reindex.js";
|
|
49
49
|
import { registerRescueCommand } from "./commands/rescue.js";
|
|
50
50
|
import { registerMcpCommand } from "./commands/mcp-status.js";
|
|
51
|
+
import { registerCrmCommand } from "./commands/crm.js";
|
|
52
|
+
import { registerCompanyCommand } from "./commands/company.js";
|
|
51
53
|
import { sanitizeArgv } from "./utils/feedback-diagnostics.js";
|
|
52
54
|
import { environmentalFsErrorMessage } from "./utils/environmental-error.js";
|
|
53
55
|
import { isEpipe } from "./utils/epipe.js";
|
|
@@ -173,6 +175,13 @@ registerRescueCommand(program);
|
|
|
173
175
|
// provenance-based status across BOTH Claude + Codex runtimes (reads `_hqPack`
|
|
174
176
|
// off the configs, NOT linkStatus), with secret-redacted output + `--json`.
|
|
175
177
|
registerMcpCommand(program);
|
|
178
|
+
// Native CRM entity upsert (subcommand group — `hq crm entity upsert`). Wraps
|
|
179
|
+
// POST /crm/entities (the ontology write gate) so an authenticated company
|
|
180
|
+
// member can create/update canonical CRM entities in the company vault.
|
|
181
|
+
registerCrmCommand(program);
|
|
182
|
+
// Company settings (subcommand group — `hq company settings set`). Owner-only
|
|
183
|
+
// toggles for crmEnabled / ontologyEnabled via PUT /company-settings.
|
|
184
|
+
registerCompanyCommand(program);
|
|
176
185
|
(async () => {
|
|
177
186
|
try {
|
|
178
187
|
Sentry.addBreadcrumb({
|
|
@@ -233,4 +242,4 @@ registerMcpCommand(program);
|
|
|
233
242
|
}
|
|
234
243
|
})();
|
|
235
244
|
//# sourceMappingURL=index.js.map
|
|
236
|
-
//# debugId=
|
|
245
|
+
//# debugId=6ad01242-c165-5be2-bb31-e929c0b38f5e
|
package/package.json
CHANGED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `hq company settings set` (company.ts).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors members.test.ts: mock ensureCognitoToken + getEntityUid, spy on
|
|
5
|
+
* global fetch, drive through a Commander program, assert the PUT
|
|
6
|
+
* /company-settings request shape + exit behavior.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
import {
|
|
11
|
+
afterEach,
|
|
12
|
+
beforeEach,
|
|
13
|
+
describe,
|
|
14
|
+
expect,
|
|
15
|
+
it,
|
|
16
|
+
vi,
|
|
17
|
+
type MockInstance,
|
|
18
|
+
} from "vitest";
|
|
19
|
+
|
|
20
|
+
vi.mock("../utils/cognito-session.js", async (importOriginal) => {
|
|
21
|
+
const original =
|
|
22
|
+
await importOriginal<typeof import("../utils/cognito-session.js")>();
|
|
23
|
+
return {
|
|
24
|
+
...original,
|
|
25
|
+
ensureCognitoToken: vi.fn(async () => "test-token"),
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
vi.mock("../utils/vault-api.js", async (importOriginal) => {
|
|
30
|
+
const original = await importOriginal<typeof import("../utils/vault-api.js")>();
|
|
31
|
+
return {
|
|
32
|
+
...original,
|
|
33
|
+
getEntityUid: vi.fn(async () => "cmp_acme"),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
38
|
+
import { getEntityUid } from "../utils/vault-api.js";
|
|
39
|
+
import { registerCompanyCommand } from "./company.js";
|
|
40
|
+
|
|
41
|
+
function jsonResponse(status: number, body: unknown): Response {
|
|
42
|
+
return new Response(JSON.stringify(body), {
|
|
43
|
+
status,
|
|
44
|
+
headers: { "Content-Type": "application/json" },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let fetchSpy: MockInstance<typeof fetch>;
|
|
49
|
+
let exitSpy: MockInstance<typeof process.exit>;
|
|
50
|
+
const mockEnsureCognitoToken = vi.mocked(ensureCognitoToken);
|
|
51
|
+
const mockGetEntityUid = vi.mocked(getEntityUid);
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
fetchSpy = vi.spyOn(globalThis, "fetch");
|
|
56
|
+
mockEnsureCognitoToken.mockResolvedValue("test-token");
|
|
57
|
+
mockGetEntityUid.mockResolvedValue("cmp_acme");
|
|
58
|
+
exitSpy = vi
|
|
59
|
+
.spyOn(process, "exit")
|
|
60
|
+
.mockImplementation((code?: number) => {
|
|
61
|
+
throw new Error(`process.exit(${code})`);
|
|
62
|
+
}) as unknown as MockInstance<typeof process.exit>;
|
|
63
|
+
vi.spyOn(console, "log").mockImplementation(() => {});
|
|
64
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
vi.restoreAllMocks();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function buildProgram(): Command {
|
|
72
|
+
const program = new Command();
|
|
73
|
+
program.name("hq").exitOverride();
|
|
74
|
+
registerCompanyCommand(program);
|
|
75
|
+
return program;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function run(args: string[]): Promise<void> {
|
|
79
|
+
await buildProgram().parseAsync(["node", "hq", ...args]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe("hq company settings set", () => {
|
|
83
|
+
it("PUTs crmEnabled=true with the resolved companyUid", async () => {
|
|
84
|
+
fetchSpy.mockResolvedValueOnce(
|
|
85
|
+
jsonResponse(200, { companySettings: { crmEnabled: true } }),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
await run([
|
|
89
|
+
"company",
|
|
90
|
+
"settings",
|
|
91
|
+
"set",
|
|
92
|
+
"--company",
|
|
93
|
+
"acme",
|
|
94
|
+
"--crm-enabled",
|
|
95
|
+
"true",
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
99
|
+
const [url, init] = fetchSpy.mock.calls[0];
|
|
100
|
+
expect(String(url)).toContain("/company-settings");
|
|
101
|
+
expect(init?.method).toBe("PUT");
|
|
102
|
+
const sent = JSON.parse(init?.body as string);
|
|
103
|
+
expect(sent).toMatchObject({ companyUid: "cmp_acme", crmEnabled: true });
|
|
104
|
+
expect(sent.ontologyEnabled).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("PUTs both flags when both are supplied", async () => {
|
|
108
|
+
fetchSpy.mockResolvedValueOnce(jsonResponse(200, {}));
|
|
109
|
+
|
|
110
|
+
await run([
|
|
111
|
+
"company",
|
|
112
|
+
"settings",
|
|
113
|
+
"set",
|
|
114
|
+
"--company",
|
|
115
|
+
"acme",
|
|
116
|
+
"--crm-enabled",
|
|
117
|
+
"false",
|
|
118
|
+
"--ontology-enabled",
|
|
119
|
+
"true",
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
const sent = JSON.parse(fetchSpy.mock.calls[0][1]?.body as string);
|
|
123
|
+
expect(sent).toMatchObject({
|
|
124
|
+
companyUid: "cmp_acme",
|
|
125
|
+
crmEnabled: false,
|
|
126
|
+
ontologyEnabled: true,
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("requires at least one flag", async () => {
|
|
131
|
+
await expect(
|
|
132
|
+
run(["company", "settings", "set", "--company", "acme"]),
|
|
133
|
+
).rejects.toThrow("process.exit(1)");
|
|
134
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("requires --company", async () => {
|
|
138
|
+
await expect(
|
|
139
|
+
run(["company", "settings", "set", "--crm-enabled", "true"]),
|
|
140
|
+
).rejects.toThrow("process.exit(1)");
|
|
141
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("rejects a non-boolean flag value", async () => {
|
|
145
|
+
await expect(
|
|
146
|
+
run([
|
|
147
|
+
"company",
|
|
148
|
+
"settings",
|
|
149
|
+
"set",
|
|
150
|
+
"--company",
|
|
151
|
+
"acme",
|
|
152
|
+
"--crm-enabled",
|
|
153
|
+
"yes",
|
|
154
|
+
]),
|
|
155
|
+
).rejects.toThrow("process.exit(1)");
|
|
156
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("exits 1 on a 403 (non-owner)", async () => {
|
|
160
|
+
fetchSpy.mockResolvedValueOnce(
|
|
161
|
+
jsonResponse(403, { error: "Requires owner role", code: "FORBIDDEN" }),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
await expect(
|
|
165
|
+
run([
|
|
166
|
+
"company",
|
|
167
|
+
"settings",
|
|
168
|
+
"set",
|
|
169
|
+
"--company",
|
|
170
|
+
"acme",
|
|
171
|
+
"--crm-enabled",
|
|
172
|
+
"true",
|
|
173
|
+
]),
|
|
174
|
+
).rejects.toThrow("process.exit(1)");
|
|
175
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
176
|
+
});
|
|
177
|
+
});
|