@indigoai-us/hq-cli 5.51.0 → 5.53.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.
- package/CHANGELOG.md +41 -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 +72 -14
- package/dist/commands/members.d.ts +10 -0
- package/dist/commands/members.js +32 -11
- 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.test.ts +130 -0
- package/src/commands/files.ts +109 -16
- package/src/commands/members.test.ts +116 -0
- package/src/commands/members.ts +40 -15
- package/src/index.ts +11 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [5.53.0]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`hq crm entity upsert` — write a client record into the native CRM from the
|
|
10
|
+
terminal.** Posts to `POST /crm/entities`, which runs through the ontology
|
|
11
|
+
write-gate (tenant-isolated, `crmEnabled`-gated) and refreshes the company's
|
|
12
|
+
CRM projection. `--company`, `--type` (`contact|deal|contract|invoice|company`)
|
|
13
|
+
and `--name` are required in flag form; `--attio-id|--stripe-id|--pandadoc-id|
|
|
14
|
+
--neon-id` populate `external_ids`; `--json <file|->` passes a full entity or
|
|
15
|
+
array instead. Surfaces a `CRM_DISABLED` 403 with an enable hint. This is the
|
|
16
|
+
build-native path for companies with no external CRM.
|
|
17
|
+
- **`hq company settings set` — toggle company settings from the CLI.** Calls
|
|
18
|
+
`PUT /company-settings` with `--crm-enabled <true|false>` and/or
|
|
19
|
+
`--ontology-enabled <true|false>` (at least one required; owner-gated
|
|
20
|
+
server-side). `--crm-enabled true` is how `/crm-setup` activates the native
|
|
21
|
+
CRM for a company.
|
|
22
|
+
|
|
23
|
+
## [5.52.0]
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **`hq members promote <target> <newRole>` (alias `set-role`) — change a
|
|
28
|
+
member's role from the CLI.** A discoverable runner for role changes via
|
|
29
|
+
`POST /membership/role`, replacing the undiscoverable, `admin|member`-only
|
|
30
|
+
`set-role` (now kept as an alias). Supports the full role set
|
|
31
|
+
(`owner|admin|member|guest`); `<target>` may be an email, a `prs_` personUid,
|
|
32
|
+
or a full membership key (same resolver as `revoke`). It is a GENERAL role
|
|
33
|
+
change — it can promote OR demote — and authorization (owner-or-admin; only
|
|
34
|
+
an owner may set a target to owner or change an owner) is enforced
|
|
35
|
+
server-side.
|
|
36
|
+
- **`hq files share --full` — glob-safe whole-vault access.** Granting prefixes
|
|
37
|
+
one at a time hit `PolicyBudgetExceeded` after a few (each prefix is a
|
|
38
|
+
distinct ARN in the member's DEFLATE-packed inline STS session policy), and
|
|
39
|
+
the intended single-`*` wildcard escape hatch was unreachable because an
|
|
40
|
+
unquoted `*` expands to local filenames and fails the one-prefix check.
|
|
41
|
+
`--full` is a glob-safe flag that performs the coalesced whole-vault wildcard
|
|
42
|
+
grant in a single policy entry.
|
|
43
|
+
|
|
44
|
+
## [5.51.0]
|
|
45
|
+
|
|
5
46
|
### Fixed
|
|
6
47
|
|
|
7
48
|
- **`hq secrets exec` / `hq secrets env` load via the batch endpoint, killing
|
|
@@ -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";
|
|
@@ -99,10 +99,35 @@ export function registerFilesCommand(program) {
|
|
|
99
99
|
.description("Share file paths. Without --with: mint a share-session URL and open it in the browser. With --with: grant access directly to a person, group, or @all.")
|
|
100
100
|
.option("--with <principal>", "Email address, group id, or '@all' to share with every active company member")
|
|
101
101
|
.option("--permission <level>", "Permission level (only with --with): read | write")
|
|
102
|
+
.option("--full", "Grant access to the ENTIRE vault (the '*' wildcard prefix) — no need to quote a glob. Requires --with; defaults to write permission.")
|
|
102
103
|
.option("--expires <duration>", "Token expiry duration for share-session URL (e.g. 15m, 1h, 24h). Default 15m. Max 24h.")
|
|
103
104
|
.option("--no-open", "Print the share-session URL but do not launch the browser")
|
|
104
105
|
.action(async (paths, opts) => {
|
|
105
106
|
try {
|
|
107
|
+
// Full-vault grant: a glob-safe affordance for "give this person the
|
|
108
|
+
// whole vault" so admins never have to quote a `*` (an unquoted glob
|
|
109
|
+
// expands to local filenames and instantly fails the one-prefix
|
|
110
|
+
// check). Maps to the single `*` wildcard grant, which the server
|
|
111
|
+
// coalesces to one policy entry — sidestepping the per-prefix STS
|
|
112
|
+
// session-policy budget. Defaults to write permission.
|
|
113
|
+
if (opts.full) {
|
|
114
|
+
if (opts.with === undefined) {
|
|
115
|
+
console.error(chalk.red("--full grants whole-vault access to a principal and requires --with <principal>."));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
if (paths && paths.length > 0) {
|
|
119
|
+
console.error(chalk.red("--full grants the entire vault; do not also pass file paths."));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
await runDirectGrant({
|
|
123
|
+
prefix: "*",
|
|
124
|
+
principal: opts.with,
|
|
125
|
+
permission: opts.permission ?? "write",
|
|
126
|
+
companySlug: files.opts().company,
|
|
127
|
+
fullVault: true,
|
|
128
|
+
});
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
106
131
|
if (!paths || paths.length === 0) {
|
|
107
132
|
console.error(chalk.red("usage: hq files share <paths...> [--with <principal>]"));
|
|
108
133
|
process.exit(1);
|
|
@@ -331,16 +356,24 @@ export function registerFilesCommand(program) {
|
|
|
331
356
|
});
|
|
332
357
|
files
|
|
333
358
|
.command("delete <prefix>")
|
|
334
|
-
.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.")
|
|
335
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)")
|
|
336
362
|
.option("-y, --yes", "Skip the confirmation prompt (for scripts)")
|
|
337
363
|
.action(async (prefix, opts) => {
|
|
338
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
|
+
}
|
|
339
371
|
await runFilesDelete({
|
|
340
372
|
prefix,
|
|
341
373
|
dryRun: opts.dryRun === true,
|
|
342
374
|
yes: opts.yes === true,
|
|
343
|
-
|
|
375
|
+
personal,
|
|
376
|
+
companySlug,
|
|
344
377
|
});
|
|
345
378
|
}
|
|
346
379
|
catch (err) {
|
|
@@ -437,7 +470,12 @@ async function runDirectGrant(params) {
|
|
|
437
470
|
const data = (await res.json());
|
|
438
471
|
const printedPrefix = data.acl?.path ?? data.acl?.prefix ?? canonicalPrefix;
|
|
439
472
|
const verb = autoCreated ? "Created ACL and granted" : "Granted";
|
|
440
|
-
|
|
473
|
+
if (params.fullVault) {
|
|
474
|
+
console.log(chalk.green(`${verb} ${params.permission} on the ENTIRE vault to ${principalLabel}`));
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
console.log(chalk.green(`${verb} ${params.permission} on ${printedPrefix} to ${principalLabel}`));
|
|
478
|
+
}
|
|
441
479
|
}
|
|
442
480
|
async function runShareSession(params) {
|
|
443
481
|
// Normalize every path through the shared prefix helper so a trailing
|
|
@@ -533,17 +571,28 @@ function realConfirm(message) {
|
|
|
533
571
|
/**
|
|
534
572
|
* POST /v1/files/delete. Throws FilesDeleteHttpError on any non-2xx so the one
|
|
535
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.
|
|
536
579
|
*/
|
|
537
580
|
async function callDeleteEndpoint(params) {
|
|
538
581
|
const res = await vaultApiFetch({
|
|
539
582
|
token: params.token,
|
|
540
583
|
path: "/v1/files/delete",
|
|
541
584
|
method: "POST",
|
|
542
|
-
body:
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
+
},
|
|
547
596
|
});
|
|
548
597
|
if (!res.ok) {
|
|
549
598
|
const body = (await res.json().catch(() => ({})));
|
|
@@ -612,14 +661,21 @@ export async function runFilesDelete(params, deps = {}) {
|
|
|
612
661
|
process.exit(1);
|
|
613
662
|
}
|
|
614
663
|
const token = await ensureCognitoToken();
|
|
615
|
-
|
|
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 };
|
|
616
672
|
// 1. Always preview first — this is how we print the EXACT key count before
|
|
617
673
|
// deleting anything (and the whole behavior of --dry-run).
|
|
618
674
|
let preview;
|
|
619
675
|
try {
|
|
620
676
|
preview = await callDeleteEndpoint({
|
|
621
677
|
token,
|
|
622
|
-
|
|
678
|
+
...scopeArgs,
|
|
623
679
|
prefix: normalized,
|
|
624
680
|
dryRun: true,
|
|
625
681
|
});
|
|
@@ -652,7 +708,9 @@ export async function runFilesDelete(params, deps = {}) {
|
|
|
652
708
|
console.log(chalk.dim(` (${preview.skipped} more under this prefix you can't delete will be left untouched)`));
|
|
653
709
|
}
|
|
654
710
|
if (!params.yes) {
|
|
655
|
-
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.`);
|
|
656
714
|
if (!ok) {
|
|
657
715
|
console.log(chalk.dim("Aborted — nothing was deleted."));
|
|
658
716
|
return;
|
|
@@ -663,7 +721,7 @@ export async function runFilesDelete(params, deps = {}) {
|
|
|
663
721
|
try {
|
|
664
722
|
result = await callDeleteEndpoint({
|
|
665
723
|
token,
|
|
666
|
-
|
|
724
|
+
...scopeArgs,
|
|
667
725
|
prefix: normalized,
|
|
668
726
|
dryRun: false,
|
|
669
727
|
});
|
|
@@ -682,4 +740,4 @@ export async function runFilesDelete(params, deps = {}) {
|
|
|
682
740
|
}
|
|
683
741
|
}
|
|
684
742
|
//# sourceMappingURL=files.js.map
|
|
685
|
-
//# debugId=
|
|
743
|
+
//# debugId=af29c08f-dbeb-5406-8deb-fdc263354b4a
|
|
@@ -149,5 +149,15 @@ export declare function listActiveMembers(token: string, companyUid: string): Pr
|
|
|
149
149
|
*/
|
|
150
150
|
export declare function resolveRevokeTargetToMembershipKey(arg: string, companyUid: string): string;
|
|
151
151
|
export declare function revokeInvite(token: string, tokenOrKey: string, companyUid: string): Promise<void>;
|
|
152
|
+
/**
|
|
153
|
+
* Change a member's role via `POST /membership/role`, accepting the FULL role
|
|
154
|
+
* set (owner|admin|member|guest). This is a GENERAL role change — it can promote
|
|
155
|
+
* OR demote. Authorization (owner-or-admin `changeRoles`, owner-only
|
|
156
|
+
* promote-to-owner / change-an-owner) is enforced SERVER-side; this function
|
|
157
|
+
* only validates the role string locally and surfaces the server's error.
|
|
158
|
+
* Role string is validated BEFORE any network call so callers/tests can rely on
|
|
159
|
+
* a synchronous-shaped rejection for a bad role.
|
|
160
|
+
*/
|
|
161
|
+
export declare function changeMemberRole(token: string, companyUid: string, membershipKey: string, newRole: Role): Promise<void>;
|
|
152
162
|
export declare function registerMembersCommand(program: Command): void;
|
|
153
163
|
//# sourceMappingURL=members.d.ts.map
|
package/dist/commands/members.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
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]="83a54ed2-3f52-5b6f-a784-09f78f488ddd")}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";
|
|
6
6
|
const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
7
7
|
const PERSON_UID_PATTERN = /^prs_[A-Za-z0-9_-]+$/;
|
|
8
8
|
export const VALID_ROLES = new Set(["owner", "admin", "member", "guest"]);
|
|
9
|
-
const VALID_MEMBER_SET_ROLES = new Set(["admin", "member"]);
|
|
10
9
|
export function detectTarget(target) {
|
|
11
10
|
if (EMAIL_PATTERN.test(target)) {
|
|
12
11
|
return { type: "email", value: target.trim().toLowerCase() };
|
|
@@ -265,9 +264,18 @@ export async function revokeInvite(token, tokenOrKey, companyUid) {
|
|
|
265
264
|
throw new InviteHttpError(res.status, err.message ?? err.error ?? res.statusText, err.code);
|
|
266
265
|
}
|
|
267
266
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
267
|
+
/**
|
|
268
|
+
* Change a member's role via `POST /membership/role`, accepting the FULL role
|
|
269
|
+
* set (owner|admin|member|guest). This is a GENERAL role change — it can promote
|
|
270
|
+
* OR demote. Authorization (owner-or-admin `changeRoles`, owner-only
|
|
271
|
+
* promote-to-owner / change-an-owner) is enforced SERVER-side; this function
|
|
272
|
+
* only validates the role string locally and surfaces the server's error.
|
|
273
|
+
* Role string is validated BEFORE any network call so callers/tests can rely on
|
|
274
|
+
* a synchronous-shaped rejection for a bad role.
|
|
275
|
+
*/
|
|
276
|
+
export async function changeMemberRole(token, companyUid, membershipKey, newRole) {
|
|
277
|
+
if (!VALID_ROLES.has(newRole)) {
|
|
278
|
+
throw new Error(`Invalid role '${newRole}': must be one of owner, admin, member, guest`);
|
|
271
279
|
}
|
|
272
280
|
const res = await vaultApiFetch({
|
|
273
281
|
token,
|
|
@@ -285,16 +293,29 @@ export function registerMembersCommand(program) {
|
|
|
285
293
|
.command("members")
|
|
286
294
|
.description("Manage company memberships and invites")
|
|
287
295
|
.option("--company <slug>", "Company slug (resolves to companyUid)");
|
|
296
|
+
// Canonical role-change command. `promote` is the discoverable verb users
|
|
297
|
+
// reach for; `set-role` is kept as an alias for the older name. The route is a
|
|
298
|
+
// GENERAL role change, so the help text and success message are honest that it
|
|
299
|
+
// can demote as well as promote. Authorization is enforced server-side.
|
|
288
300
|
members
|
|
289
|
-
.command("
|
|
290
|
-
.
|
|
291
|
-
.
|
|
301
|
+
.command("promote <target> <newRole>")
|
|
302
|
+
.alias("set-role")
|
|
303
|
+
.description("Change a member's role (owner|admin|member|guest) — promotes OR demotes. " +
|
|
304
|
+
"<target> may be an email, a prs_ personUid, or a full membership key. " +
|
|
305
|
+
"Owner-or-admin only; setting a target to owner (or changing an owner) is owner-only. Server-enforced.")
|
|
306
|
+
.action(async (target, newRole) => {
|
|
292
307
|
try {
|
|
308
|
+
const role = newRole.trim().toLowerCase();
|
|
309
|
+
if (!VALID_ROLES.has(role)) {
|
|
310
|
+
console.error(chalk.red(`Invalid role '${newRole}': must be one of owner, admin, member, guest`));
|
|
311
|
+
process.exit(1);
|
|
312
|
+
}
|
|
293
313
|
const token = await ensureCognitoToken();
|
|
294
314
|
const companySlug = members.opts().company;
|
|
295
315
|
const companyUid = await getCompanyUid(token, companySlug);
|
|
296
|
-
|
|
297
|
-
|
|
316
|
+
const membershipKey = resolveRevokeTargetToMembershipKey(target, companyUid);
|
|
317
|
+
await changeMemberRole(token, companyUid, membershipKey, role);
|
|
318
|
+
console.log(chalk.green(`Updated role for '${target}' to ${role}`));
|
|
298
319
|
}
|
|
299
320
|
catch (err) {
|
|
300
321
|
if (err instanceof InviteHttpError) {
|
|
@@ -534,4 +555,4 @@ export function registerMembersCommand(program) {
|
|
|
534
555
|
});
|
|
535
556
|
}
|
|
536
557
|
//# sourceMappingURL=members.js.map
|
|
537
|
-
//# debugId=
|
|
558
|
+
//# debugId=83a54ed2-3f52-5b6f-a784-09f78f488ddd
|
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
|