@indigoai-us/hq-cli 5.6.3 → 5.8.3
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/dist/commands/_patterns.d.ts +2 -0
- package/dist/commands/_patterns.js +9 -2
- package/dist/commands/cloud.js +100 -11
- package/dist/commands/files.d.ts +3 -0
- package/dist/commands/files.js +206 -0
- package/dist/index.js +6 -3
- package/dist/utils/cognito-session.js +3 -4
- package/package.json +1 -1
- package/src/commands/_patterns.ts +8 -0
- package/src/commands/cloud.ts +119 -12
- package/src/commands/files.ts +227 -0
- package/src/index.ts +5 -1
- package/src/utils/cognito-session.ts +1 -2
|
@@ -1,6 +1,13 @@
|
|
|
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]="0d9c591b-d54a-52b4-85c3-32a9030953ca")}catch(e){}}();
|
|
3
3
|
export const SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]*(?:\/[A-Z][A-Z0-9_]+)*$/;
|
|
4
4
|
export const GROUP_ID_PATTERN = /^grp_[A-Za-z0-9_-]+$/;
|
|
5
|
+
export const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
6
|
+
export function normalizeFilePrefix(prefix) {
|
|
7
|
+
if (prefix.endsWith("/")) {
|
|
8
|
+
return prefix + "*";
|
|
9
|
+
}
|
|
10
|
+
return prefix;
|
|
11
|
+
}
|
|
5
12
|
//# sourceMappingURL=_patterns.js.map
|
|
6
|
-
//# debugId=
|
|
13
|
+
//# debugId=0d9c591b-d54a-52b4-85c3-32a9030953ca
|
package/dist/commands/cloud.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* hq sync status — show local journal summary
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
!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]="
|
|
16
|
+
!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]="5ddbce6a-fdde-506e-8cdd-2e5b0ad20fff")}catch(e){}}();
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import * as fs from "fs";
|
|
19
19
|
import * as path from "path";
|
|
@@ -28,30 +28,104 @@ export function registerCloudCommands(program) {
|
|
|
28
28
|
.option("--company <slug>", "Company slug or UID (defaults to active company in .hq/config.json)")
|
|
29
29
|
.option("--message <msg>", "Optional message attached to journal entries for these uploads")
|
|
30
30
|
.option("--on-conflict <strategy>", "Conflict strategy: overwrite | keep | abort (omit for interactive)")
|
|
31
|
+
.option("--creds-from-stdin", "Read a pre-vended EntityContext as JSON from stdin instead of vending " +
|
|
32
|
+
"via the cached Cognito session. Use when the caller (e.g. AppBar HQ " +
|
|
33
|
+
"Sync) has its own STS pipeline (`/sts/vend-child` with task scope) " +
|
|
34
|
+
"and just needs share()'s upload mechanics. The caller is responsible " +
|
|
35
|
+
"for vending credentials with enough TTL for the run.")
|
|
36
|
+
.option("--json", "Emit each share()-level event as a JSON Lines record on stderr (one " +
|
|
37
|
+
"JSON object per line) instead of human-readable console output. A " +
|
|
38
|
+
"synthetic `{type:\"complete\",...}` line is appended at the end with " +
|
|
39
|
+
"the final ShareResult. Subprocess callers parse these to render their " +
|
|
40
|
+
"own UI (e.g. AppBar Tauri events).")
|
|
31
41
|
.action(async (paths, options) => {
|
|
42
|
+
const jsonMode = options.json === true;
|
|
43
|
+
// Suppress the human banner/result output in JSON mode — the parent
|
|
44
|
+
// process renders its own UI from the stderr ndjson stream.
|
|
45
|
+
const log = (msg) => {
|
|
46
|
+
if (!jsonMode)
|
|
47
|
+
console.log(msg);
|
|
48
|
+
};
|
|
49
|
+
const emitJson = (event) => {
|
|
50
|
+
process.stderr.write(JSON.stringify(event) + "\n");
|
|
51
|
+
};
|
|
32
52
|
try {
|
|
33
53
|
const targetPaths = paths && paths.length > 0 ? paths : [process.cwd()];
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
54
|
+
log(chalk.bold("\nHQ Sync — Push"));
|
|
55
|
+
log(` HQ root: ${options.hqRoot}`);
|
|
56
|
+
log(` Company: ${options.company ?? "(from .hq/config.json or stdin)"}`);
|
|
57
|
+
log(` Paths: ${targetPaths.join(", ")}\n`);
|
|
58
|
+
// Resolve credentials. Two paths:
|
|
59
|
+
// 1. --creds-from-stdin: parse JSON EntityContext from stdin (the
|
|
60
|
+
// AppBar shell-out contract — vend-child upstream, pipe in here).
|
|
61
|
+
// 2. default: vend via cached Cognito session (the human CLI path).
|
|
62
|
+
let entityContext;
|
|
63
|
+
let vaultConfig;
|
|
64
|
+
if (options.credsFromStdin) {
|
|
65
|
+
if (process.stdin.isTTY) {
|
|
66
|
+
throw new Error("--creds-from-stdin requires JSON on stdin, but stdin is a " +
|
|
67
|
+
"TTY. Pipe the EntityContext JSON via subprocess stdin " +
|
|
68
|
+
"(e.g. `echo '{...}' | hq sync push --creds-from-stdin ...`).");
|
|
69
|
+
}
|
|
70
|
+
const raw = await readAllStdin();
|
|
71
|
+
try {
|
|
72
|
+
entityContext = JSON.parse(raw);
|
|
73
|
+
}
|
|
74
|
+
catch (e) {
|
|
75
|
+
throw new Error(`--creds-from-stdin: failed to parse stdin as JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const accessToken = await ensureCognitoToken();
|
|
80
|
+
vaultConfig = buildVaultConfig(accessToken);
|
|
81
|
+
}
|
|
82
|
+
// In JSON mode, forward every share() event verbatim to stderr as
|
|
83
|
+
// ndjson. In human mode, share()'s defaultConsoleLogger handles the
|
|
84
|
+
// rendering (no onEvent → falls through to stdout/stderr printing).
|
|
85
|
+
const onEvent = jsonMode
|
|
86
|
+
? (event) => emitJson(event)
|
|
87
|
+
: undefined;
|
|
39
88
|
const result = await share({
|
|
40
89
|
paths: targetPaths,
|
|
41
90
|
company: options.company,
|
|
42
91
|
message: options.message,
|
|
43
92
|
onConflict: options.onConflict,
|
|
44
|
-
vaultConfig
|
|
93
|
+
vaultConfig,
|
|
94
|
+
entityContext,
|
|
45
95
|
hqRoot: options.hqRoot,
|
|
96
|
+
onEvent,
|
|
46
97
|
});
|
|
98
|
+
if (jsonMode) {
|
|
99
|
+
// Synthetic terminal event so subprocess consumers can read final
|
|
100
|
+
// counts without summing per-file events. Distinguished from
|
|
101
|
+
// SyncProgressEvent by `type:"complete"` (not in the share()
|
|
102
|
+
// event schema — added at the CLI seam).
|
|
103
|
+
emitJson({
|
|
104
|
+
type: "complete",
|
|
105
|
+
filesUploaded: result.filesUploaded,
|
|
106
|
+
bytesUploaded: result.bytesUploaded,
|
|
107
|
+
filesSkipped: result.filesSkipped,
|
|
108
|
+
conflictPaths: result.conflictPaths,
|
|
109
|
+
aborted: result.aborted,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
47
112
|
if (result.aborted) {
|
|
48
|
-
|
|
113
|
+
log(chalk.yellow(`\n⚠ Push aborted (${result.filesUploaded} uploaded, ${result.filesSkipped} skipped)`));
|
|
49
114
|
process.exit(1);
|
|
50
115
|
}
|
|
51
|
-
|
|
116
|
+
log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`));
|
|
52
117
|
}
|
|
53
118
|
catch (err) {
|
|
54
|
-
|
|
119
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
120
|
+
if (jsonMode) {
|
|
121
|
+
// In JSON mode, the parent process is parsing stderr for ndjson —
|
|
122
|
+
// human-formatted error lines would corrupt the stream. Emit a
|
|
123
|
+
// structured `fatal` event instead and let the parent surface it.
|
|
124
|
+
emitJson({ type: "fatal", message });
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
console.error(chalk.red("\n✗ Push failed:"), message);
|
|
128
|
+
}
|
|
55
129
|
process.exit(1);
|
|
56
130
|
}
|
|
57
131
|
});
|
|
@@ -137,5 +211,20 @@ function formatBytes(bytes) {
|
|
|
137
211
|
const value = bytes / Math.pow(1024, exponent);
|
|
138
212
|
return `${value.toFixed(value >= 100 || exponent === 0 ? 0 : 1)} ${units[exponent]}`;
|
|
139
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Read all of stdin as a UTF-8 string. Used by `--creds-from-stdin` to
|
|
216
|
+
* receive a JSON-serialized EntityContext from the parent process (e.g.
|
|
217
|
+
* AppBar HQ Sync). Returns the empty string when stdin closes immediately.
|
|
218
|
+
*
|
|
219
|
+
* Caller is expected to detect TTY first — this function will block forever
|
|
220
|
+
* waiting for stdin to close if invoked interactively.
|
|
221
|
+
*/
|
|
222
|
+
async function readAllStdin() {
|
|
223
|
+
const chunks = [];
|
|
224
|
+
for await (const chunk of process.stdin) {
|
|
225
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
226
|
+
}
|
|
227
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
228
|
+
}
|
|
140
229
|
//# sourceMappingURL=cloud.js.map
|
|
141
|
-
//# debugId=
|
|
230
|
+
//# debugId=5ddbce6a-fdde-506e-8cdd-2e5b0ad20fff
|
|
@@ -0,0 +1,206 @@
|
|
|
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]="5f7a9620-b2ba-5186-980c-76df147440f0")}catch(e){}}();
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
5
|
+
import { vaultApiFetch, getCompanyUid } from "./secrets.js";
|
|
6
|
+
import { GROUP_ID_PATTERN, EMAIL_PATTERN, normalizeFilePrefix } from "./_patterns.js";
|
|
7
|
+
export function registerFilesCommand(program) {
|
|
8
|
+
const files = program
|
|
9
|
+
.command("files")
|
|
10
|
+
.description("Manage file access controls in HQ vault")
|
|
11
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)");
|
|
12
|
+
files
|
|
13
|
+
.command("share <prefix>")
|
|
14
|
+
.description("Share a file prefix with a person or group")
|
|
15
|
+
.requiredOption("--with <principal>", "Email address or group id to share with")
|
|
16
|
+
.requiredOption("--permission <level>", "Permission level: read | write")
|
|
17
|
+
.action(async (prefix, opts) => {
|
|
18
|
+
try {
|
|
19
|
+
const canonicalPrefix = normalizeFilePrefix(prefix);
|
|
20
|
+
if (!["read", "write"].includes(opts.permission)) {
|
|
21
|
+
console.error(chalk.red(`Invalid permission '${opts.permission}': must be one of read, write`));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const principal = opts.with;
|
|
25
|
+
const isEmail = EMAIL_PATTERN.test(principal);
|
|
26
|
+
const isGroup = GROUP_ID_PATTERN.test(principal);
|
|
27
|
+
if (!isEmail && !isGroup) {
|
|
28
|
+
console.error(chalk.red(`Invalid principal '${principal}': must be an email address or a group id matching grp_<alphanumeric>`));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const granteeType = isEmail ? "email" : "group";
|
|
32
|
+
const granteeId = isEmail ? principal.trim().toLowerCase() : principal;
|
|
33
|
+
const token = await ensureCognitoToken();
|
|
34
|
+
const companySlug = files.opts().company;
|
|
35
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
36
|
+
const res = await vaultApiFetch({
|
|
37
|
+
token,
|
|
38
|
+
path: `/files/${encodeURIComponent(companyUid)}/acl/grant`,
|
|
39
|
+
method: "POST",
|
|
40
|
+
body: { prefix: canonicalPrefix, granteeType, granteeId, permission: opts.permission },
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) {
|
|
43
|
+
const body = await res.json().catch(() => ({}));
|
|
44
|
+
if (res.status === 401) {
|
|
45
|
+
console.error(chalk.red("Not authenticated — please run `hq login`"));
|
|
46
|
+
}
|
|
47
|
+
else if (res.status === 403) {
|
|
48
|
+
console.error(chalk.red("Not authorized to share this file prefix"));
|
|
49
|
+
}
|
|
50
|
+
else if (res.status === 404) {
|
|
51
|
+
console.error(chalk.red("ACL record not found — the prefix may not have an ACL yet"));
|
|
52
|
+
}
|
|
53
|
+
else if (res.status === 409) {
|
|
54
|
+
console.error(chalk.red("Concurrent modification — please retry"));
|
|
55
|
+
}
|
|
56
|
+
else if (res.status >= 500) {
|
|
57
|
+
console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
console.error(chalk.red(body.message ?? body.error ?? "Invalid request"));
|
|
61
|
+
}
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const data = await res.json();
|
|
65
|
+
const printedPrefix = data.acl?.prefix ?? canonicalPrefix;
|
|
66
|
+
console.log(chalk.green(`Granted ${opts.permission} on ${printedPrefix} to ${granteeId}`));
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
files
|
|
74
|
+
.command("unshare <prefix>")
|
|
75
|
+
.description("Remove a file access grant")
|
|
76
|
+
.requiredOption("--with <principal>", "Email address or group id to remove")
|
|
77
|
+
.action(async (prefix, opts) => {
|
|
78
|
+
try {
|
|
79
|
+
const canonicalPrefix = normalizeFilePrefix(prefix);
|
|
80
|
+
const principal = opts.with;
|
|
81
|
+
const isEmail = EMAIL_PATTERN.test(principal);
|
|
82
|
+
const isGroup = GROUP_ID_PATTERN.test(principal);
|
|
83
|
+
if (!isEmail && !isGroup) {
|
|
84
|
+
console.error(chalk.red(`Invalid principal '${principal}': must be an email address or a group id matching grp_<alphanumeric>`));
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
const granteeType = isEmail ? "email" : "group";
|
|
88
|
+
const granteeId = isEmail ? principal.trim().toLowerCase() : principal;
|
|
89
|
+
const token = await ensureCognitoToken();
|
|
90
|
+
const companySlug = files.opts().company;
|
|
91
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
92
|
+
const res = await vaultApiFetch({
|
|
93
|
+
token,
|
|
94
|
+
path: `/files/${encodeURIComponent(companyUid)}/acl/revoke`,
|
|
95
|
+
method: "POST",
|
|
96
|
+
body: { prefix: canonicalPrefix, granteeType, granteeId },
|
|
97
|
+
});
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
const body = await res.json().catch(() => ({}));
|
|
100
|
+
if (res.status === 401) {
|
|
101
|
+
console.error(chalk.red("Not authenticated — please run `hq login`"));
|
|
102
|
+
}
|
|
103
|
+
else if (res.status === 403) {
|
|
104
|
+
console.error(chalk.red("Not authorized to modify this file prefix's ACL"));
|
|
105
|
+
}
|
|
106
|
+
else if (res.status === 404) {
|
|
107
|
+
console.log(chalk.green(`Grant already absent for '${canonicalPrefix}' / ${granteeId}`));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
else if (res.status >= 500) {
|
|
111
|
+
console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
console.error(chalk.red(body.message ?? body.error ?? "Invalid request"));
|
|
115
|
+
}
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
console.log(chalk.green(`Removed grant for ${granteeId} on '${canonicalPrefix}'`));
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
files
|
|
126
|
+
.command("acl <prefix>")
|
|
127
|
+
.description("Show the ACL (access control list) for a file prefix")
|
|
128
|
+
.action(async (prefix) => {
|
|
129
|
+
try {
|
|
130
|
+
const canonicalPrefix = normalizeFilePrefix(prefix);
|
|
131
|
+
const token = await ensureCognitoToken();
|
|
132
|
+
const companySlug = files.opts().company;
|
|
133
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
134
|
+
const res = await vaultApiFetch({
|
|
135
|
+
token,
|
|
136
|
+
path: `/files/${encodeURIComponent(companyUid)}/acl`,
|
|
137
|
+
query: { prefix: canonicalPrefix },
|
|
138
|
+
});
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
const body = await res.json().catch(() => ({}));
|
|
141
|
+
if (res.status === 401) {
|
|
142
|
+
console.error(chalk.red("Not authenticated — please run `hq login`"));
|
|
143
|
+
}
|
|
144
|
+
else if (res.status === 403) {
|
|
145
|
+
console.error(chalk.red("Not authorized to view this file prefix's ACL"));
|
|
146
|
+
}
|
|
147
|
+
else if (res.status === 404) {
|
|
148
|
+
console.error(chalk.red(`No ACL record exists for '${canonicalPrefix}'`));
|
|
149
|
+
}
|
|
150
|
+
else if (res.status >= 500) {
|
|
151
|
+
console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
console.error(chalk.red(body.message ?? body.error ?? "Invalid request"));
|
|
155
|
+
}
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
const data = await res.json();
|
|
159
|
+
const acl = data.acl;
|
|
160
|
+
const aclStatus = acl.open ? "open" : "restricted";
|
|
161
|
+
console.log(chalk.green(`ACL for ${acl.prefix} (${aclStatus})`));
|
|
162
|
+
console.log(`Creator: ${acl.creatorUid}`);
|
|
163
|
+
if (acl.effectivePermission) {
|
|
164
|
+
console.log(`Your effective permission: ${acl.effectivePermission}`);
|
|
165
|
+
}
|
|
166
|
+
if (acl.entries.length === 0) {
|
|
167
|
+
if (acl.open) {
|
|
168
|
+
console.log(chalk.gray("Open ACL — all active members have read access."));
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
console.log(chalk.gray("No explicit grants — only creator has access."));
|
|
172
|
+
}
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
console.log("Entries:");
|
|
176
|
+
const TYPE_W = Math.max(4, ...acl.entries.map((e) => e.granteeType.length));
|
|
177
|
+
const GRANTEE_W = Math.max(7, ...acl.entries.map((e) => e.granteeId.length));
|
|
178
|
+
const PERM_W = Math.max(10, ...acl.entries.map((e) => e.permission.length));
|
|
179
|
+
const BY_W = Math.max(10, ...acl.entries.map((e) => e.grantedBy.length));
|
|
180
|
+
const tableHeader = [
|
|
181
|
+
"TYPE".padEnd(TYPE_W),
|
|
182
|
+
"GRANTEE".padEnd(GRANTEE_W),
|
|
183
|
+
"PERMISSION".padEnd(PERM_W),
|
|
184
|
+
"GRANTED_BY".padEnd(BY_W),
|
|
185
|
+
"GRANTED_AT",
|
|
186
|
+
].join(" ");
|
|
187
|
+
console.log(chalk.bold(tableHeader));
|
|
188
|
+
for (const e of acl.entries) {
|
|
189
|
+
const grantedAt = e.grantedAt.slice(0, 10);
|
|
190
|
+
console.log([
|
|
191
|
+
e.granteeType.padEnd(TYPE_W),
|
|
192
|
+
e.granteeId.padEnd(GRANTEE_W),
|
|
193
|
+
e.permission.padEnd(PERM_W),
|
|
194
|
+
e.grantedBy.padEnd(BY_W),
|
|
195
|
+
grantedAt,
|
|
196
|
+
].join(" "));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=files.js.map
|
|
206
|
+
//# debugId=5f7a9620-b2ba-5186-980c-76df147440f0
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* HQ CLI - Module management, package management, and cloud sync for HQ
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
!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]="
|
|
6
|
+
!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]="79222b07-0f20-582f-962f-6bc13b8c1c88")}catch(e){}}();
|
|
7
7
|
import { Command } from "commander";
|
|
8
8
|
import { initSentry, Sentry } from "./sentry.js";
|
|
9
9
|
import { registerAddCommand } from "./commands/add.js";
|
|
@@ -24,12 +24,13 @@ import { registerTeamSyncCommand } from "./commands/team-sync.js";
|
|
|
24
24
|
import { registerAuthCommands } from "./commands/auth.js";
|
|
25
25
|
import { registerSecretsCommand } from "./commands/secrets.js";
|
|
26
26
|
import { registerGroupsCommand } from "./commands/groups.js";
|
|
27
|
+
import { registerFilesCommand } from "./commands/files.js";
|
|
27
28
|
initSentry();
|
|
28
29
|
const program = new Command();
|
|
29
30
|
program
|
|
30
31
|
.name("hq")
|
|
31
32
|
.description("HQ management CLI — modules, packages, and cloud sync")
|
|
32
|
-
.version("5.
|
|
33
|
+
.version("5.8.3");
|
|
33
34
|
// Module management subcommand group
|
|
34
35
|
const modulesCmd = program
|
|
35
36
|
.command("modules")
|
|
@@ -73,6 +74,8 @@ registerAuthCommands(program);
|
|
|
73
74
|
registerSecretsCommand(program);
|
|
74
75
|
// Groups management (subcommand group — hq groups create|delete|add|remove|list|members)
|
|
75
76
|
registerGroupsCommand(program);
|
|
77
|
+
// Files ACL management (subcommand group — hq files share|unshare|acl)
|
|
78
|
+
registerFilesCommand(program);
|
|
76
79
|
// Onboarding (top-level — Cognito + vault-service provisioning)
|
|
77
80
|
registerOnboardCommand(program);
|
|
78
81
|
(async () => {
|
|
@@ -88,4 +91,4 @@ registerOnboardCommand(program);
|
|
|
88
91
|
}
|
|
89
92
|
})();
|
|
90
93
|
//# sourceMappingURL=index.js.map
|
|
91
|
-
//# debugId=
|
|
94
|
+
//# debugId=79222b07-0f20-582f-962f-6bc13b8c1c88
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* HQ_VAULT_API_URL — vault-service API Gateway URL
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
!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]="
|
|
22
|
+
!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]="71d8f748-f249-50df-aa1f-eae381ea608d")}catch(e){}}();
|
|
23
23
|
import * as os from "os";
|
|
24
24
|
import * as path from "path";
|
|
25
25
|
import chalk from "chalk";
|
|
@@ -40,8 +40,7 @@ export const DEFAULT_COGNITO = {
|
|
|
40
40
|
? process.env.HQ_COGNITO_IDENTITY_PROVIDER || undefined
|
|
41
41
|
: "Google",
|
|
42
42
|
};
|
|
43
|
-
export const DEFAULT_VAULT_API_URL = process.env.HQ_VAULT_API_URL ??
|
|
44
|
-
"https://4nfy67z28h.execute-api.us-east-1.amazonaws.com";
|
|
43
|
+
export const DEFAULT_VAULT_API_URL = process.env.HQ_VAULT_API_URL ?? "https://hqapi.getindigo.ai";
|
|
45
44
|
export const DEFAULT_HQ_ROOT = path.join(os.homedir(), "hq");
|
|
46
45
|
/**
|
|
47
46
|
* Return a non-expired Cognito access token, refreshing or browser-logging-in
|
|
@@ -109,4 +108,4 @@ export async function refreshCachedSession() {
|
|
|
109
108
|
}
|
|
110
109
|
}
|
|
111
110
|
//# sourceMappingURL=cognito-session.js.map
|
|
112
|
-
//# debugId=
|
|
111
|
+
//# debugId=71d8f748-f249-50df-aa1f-eae381ea608d
|
package/package.json
CHANGED
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
export const SECRET_NAME_PATTERN = /^[A-Z][A-Z0-9_]*(?:\/[A-Z][A-Z0-9_]+)*$/;
|
|
2
2
|
export const GROUP_ID_PATTERN = /^grp_[A-Za-z0-9_-]+$/;
|
|
3
|
+
export const EMAIL_PATTERN = /^[^\s]+@[^\s]+$/;
|
|
4
|
+
|
|
5
|
+
export function normalizeFilePrefix(prefix: string): string {
|
|
6
|
+
if (prefix.endsWith("/")) {
|
|
7
|
+
return prefix + "*";
|
|
8
|
+
}
|
|
9
|
+
return prefix;
|
|
10
|
+
}
|
package/src/commands/cloud.ts
CHANGED
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
readJournal,
|
|
25
25
|
getJournalPath,
|
|
26
26
|
type ConflictStrategy,
|
|
27
|
+
type EntityContext,
|
|
28
|
+
type SyncProgressEvent,
|
|
27
29
|
} from "@indigoai-us/hq-cloud";
|
|
28
30
|
|
|
29
31
|
import {
|
|
@@ -59,35 +61,119 @@ export function registerCloudCommands(program: Command): void {
|
|
|
59
61
|
"--on-conflict <strategy>",
|
|
60
62
|
"Conflict strategy: overwrite | keep | abort (omit for interactive)",
|
|
61
63
|
)
|
|
64
|
+
.option(
|
|
65
|
+
"--creds-from-stdin",
|
|
66
|
+
"Read a pre-vended EntityContext as JSON from stdin instead of vending " +
|
|
67
|
+
"via the cached Cognito session. Use when the caller (e.g. AppBar HQ " +
|
|
68
|
+
"Sync) has its own STS pipeline (`/sts/vend-child` with task scope) " +
|
|
69
|
+
"and just needs share()'s upload mechanics. The caller is responsible " +
|
|
70
|
+
"for vending credentials with enough TTL for the run.",
|
|
71
|
+
)
|
|
72
|
+
.option(
|
|
73
|
+
"--json",
|
|
74
|
+
"Emit each share()-level event as a JSON Lines record on stderr (one " +
|
|
75
|
+
"JSON object per line) instead of human-readable console output. A " +
|
|
76
|
+
"synthetic `{type:\"complete\",...}` line is appended at the end with " +
|
|
77
|
+
"the final ShareResult. Subprocess callers parse these to render their " +
|
|
78
|
+
"own UI (e.g. AppBar Tauri events).",
|
|
79
|
+
)
|
|
62
80
|
.action(
|
|
63
81
|
async (
|
|
64
82
|
paths: string[],
|
|
65
83
|
options: CommonSyncOptions & {
|
|
66
84
|
message?: string;
|
|
67
85
|
onConflict?: ConflictStrategy;
|
|
86
|
+
credsFromStdin?: boolean;
|
|
87
|
+
json?: boolean;
|
|
68
88
|
},
|
|
69
89
|
) => {
|
|
90
|
+
const jsonMode = options.json === true;
|
|
91
|
+
// Suppress the human banner/result output in JSON mode — the parent
|
|
92
|
+
// process renders its own UI from the stderr ndjson stream.
|
|
93
|
+
const log = (msg: string): void => {
|
|
94
|
+
if (!jsonMode) console.log(msg);
|
|
95
|
+
};
|
|
96
|
+
const emitJson = (event: Record<string, unknown>): void => {
|
|
97
|
+
process.stderr.write(JSON.stringify(event) + "\n");
|
|
98
|
+
};
|
|
99
|
+
|
|
70
100
|
try {
|
|
71
101
|
const targetPaths =
|
|
72
102
|
paths && paths.length > 0 ? paths : [process.cwd()];
|
|
73
103
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
104
|
+
log(chalk.bold("\nHQ Sync — Push"));
|
|
105
|
+
log(` HQ root: ${options.hqRoot}`);
|
|
106
|
+
log(
|
|
107
|
+
` Company: ${options.company ?? "(from .hq/config.json or stdin)"}`,
|
|
108
|
+
);
|
|
109
|
+
log(` Paths: ${targetPaths.join(", ")}\n`);
|
|
110
|
+
|
|
111
|
+
// Resolve credentials. Two paths:
|
|
112
|
+
// 1. --creds-from-stdin: parse JSON EntityContext from stdin (the
|
|
113
|
+
// AppBar shell-out contract — vend-child upstream, pipe in here).
|
|
114
|
+
// 2. default: vend via cached Cognito session (the human CLI path).
|
|
115
|
+
let entityContext: EntityContext | undefined;
|
|
116
|
+
let vaultConfig: ReturnType<typeof buildVaultConfig> | undefined;
|
|
117
|
+
|
|
118
|
+
if (options.credsFromStdin) {
|
|
119
|
+
if (process.stdin.isTTY) {
|
|
120
|
+
throw new Error(
|
|
121
|
+
"--creds-from-stdin requires JSON on stdin, but stdin is a " +
|
|
122
|
+
"TTY. Pipe the EntityContext JSON via subprocess stdin " +
|
|
123
|
+
"(e.g. `echo '{...}' | hq sync push --creds-from-stdin ...`).",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const raw = await readAllStdin();
|
|
127
|
+
try {
|
|
128
|
+
entityContext = JSON.parse(raw) as EntityContext;
|
|
129
|
+
} catch (e) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`--creds-from-stdin: failed to parse stdin as JSON: ${
|
|
132
|
+
e instanceof Error ? e.message : String(e)
|
|
133
|
+
}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
} else {
|
|
137
|
+
const accessToken = await ensureCognitoToken();
|
|
138
|
+
vaultConfig = buildVaultConfig(accessToken);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// In JSON mode, forward every share() event verbatim to stderr as
|
|
142
|
+
// ndjson. In human mode, share()'s defaultConsoleLogger handles the
|
|
143
|
+
// rendering (no onEvent → falls through to stdout/stderr printing).
|
|
144
|
+
const onEvent = jsonMode
|
|
145
|
+
? (event: SyncProgressEvent): void =>
|
|
146
|
+
emitJson(event as unknown as Record<string, unknown>)
|
|
147
|
+
: undefined;
|
|
78
148
|
|
|
79
|
-
const accessToken = await ensureCognitoToken();
|
|
80
149
|
const result = await share({
|
|
81
150
|
paths: targetPaths,
|
|
82
151
|
company: options.company,
|
|
83
152
|
message: options.message,
|
|
84
153
|
onConflict: options.onConflict,
|
|
85
|
-
vaultConfig
|
|
154
|
+
vaultConfig,
|
|
155
|
+
entityContext,
|
|
86
156
|
hqRoot: options.hqRoot,
|
|
157
|
+
onEvent,
|
|
87
158
|
});
|
|
88
159
|
|
|
160
|
+
if (jsonMode) {
|
|
161
|
+
// Synthetic terminal event so subprocess consumers can read final
|
|
162
|
+
// counts without summing per-file events. Distinguished from
|
|
163
|
+
// SyncProgressEvent by `type:"complete"` (not in the share()
|
|
164
|
+
// event schema — added at the CLI seam).
|
|
165
|
+
emitJson({
|
|
166
|
+
type: "complete",
|
|
167
|
+
filesUploaded: result.filesUploaded,
|
|
168
|
+
bytesUploaded: result.bytesUploaded,
|
|
169
|
+
filesSkipped: result.filesSkipped,
|
|
170
|
+
conflictPaths: result.conflictPaths,
|
|
171
|
+
aborted: result.aborted,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
89
175
|
if (result.aborted) {
|
|
90
|
-
|
|
176
|
+
log(
|
|
91
177
|
chalk.yellow(
|
|
92
178
|
`\n⚠ Push aborted (${result.filesUploaded} uploaded, ${result.filesSkipped} skipped)`,
|
|
93
179
|
),
|
|
@@ -95,16 +181,21 @@ export function registerCloudCommands(program: Command): void {
|
|
|
95
181
|
process.exit(1);
|
|
96
182
|
}
|
|
97
183
|
|
|
98
|
-
|
|
184
|
+
log(
|
|
99
185
|
chalk.green(
|
|
100
186
|
`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`,
|
|
101
187
|
),
|
|
102
188
|
);
|
|
103
189
|
} catch (err) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
190
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
191
|
+
if (jsonMode) {
|
|
192
|
+
// In JSON mode, the parent process is parsing stderr for ndjson —
|
|
193
|
+
// human-formatted error lines would corrupt the stream. Emit a
|
|
194
|
+
// structured `fatal` event instead and let the parent surface it.
|
|
195
|
+
emitJson({ type: "fatal", message });
|
|
196
|
+
} else {
|
|
197
|
+
console.error(chalk.red("\n✗ Push failed:"), message);
|
|
198
|
+
}
|
|
108
199
|
process.exit(1);
|
|
109
200
|
}
|
|
110
201
|
},
|
|
@@ -236,3 +327,19 @@ function formatBytes(bytes: number): string {
|
|
|
236
327
|
const value = bytes / Math.pow(1024, exponent);
|
|
237
328
|
return `${value.toFixed(value >= 100 || exponent === 0 ? 0 : 1)} ${units[exponent]}`;
|
|
238
329
|
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Read all of stdin as a UTF-8 string. Used by `--creds-from-stdin` to
|
|
333
|
+
* receive a JSON-serialized EntityContext from the parent process (e.g.
|
|
334
|
+
* AppBar HQ Sync). Returns the empty string when stdin closes immediately.
|
|
335
|
+
*
|
|
336
|
+
* Caller is expected to detect TTY first — this function will block forever
|
|
337
|
+
* waiting for stdin to close if invoked interactively.
|
|
338
|
+
*/
|
|
339
|
+
async function readAllStdin(): Promise<string> {
|
|
340
|
+
const chunks: Buffer[] = [];
|
|
341
|
+
for await (const chunk of process.stdin) {
|
|
342
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
343
|
+
}
|
|
344
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
345
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import { ensureCognitoToken } from "../utils/cognito-session.js";
|
|
4
|
+
import { vaultApiFetch, getCompanyUid } from "./secrets.js";
|
|
5
|
+
import { GROUP_ID_PATTERN, EMAIL_PATTERN, normalizeFilePrefix } from "./_patterns.js";
|
|
6
|
+
|
|
7
|
+
export function registerFilesCommand(program: Command): void {
|
|
8
|
+
const files = program
|
|
9
|
+
.command("files")
|
|
10
|
+
.description("Manage file access controls in HQ vault")
|
|
11
|
+
.option("--company <slug>", "Company slug (resolves to companyUid)");
|
|
12
|
+
|
|
13
|
+
files
|
|
14
|
+
.command("share <prefix>")
|
|
15
|
+
.description("Share a file prefix with a person or group")
|
|
16
|
+
.requiredOption("--with <principal>", "Email address or group id to share with")
|
|
17
|
+
.requiredOption("--permission <level>", "Permission level: read | write")
|
|
18
|
+
.action(async (prefix: string, opts: { with: string; permission: string }) => {
|
|
19
|
+
try {
|
|
20
|
+
const canonicalPrefix = normalizeFilePrefix(prefix);
|
|
21
|
+
|
|
22
|
+
if (!["read", "write"].includes(opts.permission)) {
|
|
23
|
+
console.error(chalk.red(`Invalid permission '${opts.permission}': must be one of read, write`));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const principal = opts.with;
|
|
28
|
+
const isEmail = EMAIL_PATTERN.test(principal);
|
|
29
|
+
const isGroup = GROUP_ID_PATTERN.test(principal);
|
|
30
|
+
if (!isEmail && !isGroup) {
|
|
31
|
+
console.error(chalk.red(`Invalid principal '${principal}': must be an email address or a group id matching grp_<alphanumeric>`));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const granteeType = isEmail ? "email" : "group";
|
|
35
|
+
const granteeId = isEmail ? principal.trim().toLowerCase() : principal;
|
|
36
|
+
|
|
37
|
+
const token = await ensureCognitoToken();
|
|
38
|
+
const companySlug = files.opts().company as string | undefined;
|
|
39
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
40
|
+
|
|
41
|
+
const res = await vaultApiFetch({
|
|
42
|
+
token,
|
|
43
|
+
path: `/files/${encodeURIComponent(companyUid)}/acl/grant`,
|
|
44
|
+
method: "POST",
|
|
45
|
+
body: { prefix: canonicalPrefix, granteeType, granteeId, permission: opts.permission },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
const body = await res.json().catch(() => ({})) as Record<string, string>;
|
|
50
|
+
if (res.status === 401) {
|
|
51
|
+
console.error(chalk.red("Not authenticated — please run `hq login`"));
|
|
52
|
+
} else if (res.status === 403) {
|
|
53
|
+
console.error(chalk.red("Not authorized to share this file prefix"));
|
|
54
|
+
} else if (res.status === 404) {
|
|
55
|
+
console.error(chalk.red("ACL record not found — the prefix may not have an ACL yet"));
|
|
56
|
+
} else if (res.status === 409) {
|
|
57
|
+
console.error(chalk.red("Concurrent modification — please retry"));
|
|
58
|
+
} else if (res.status >= 500) {
|
|
59
|
+
console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
|
|
60
|
+
} else {
|
|
61
|
+
console.error(chalk.red(body.message ?? body.error ?? "Invalid request"));
|
|
62
|
+
}
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data = await res.json() as { acl?: { prefix?: string } };
|
|
67
|
+
const printedPrefix = data.acl?.prefix ?? canonicalPrefix;
|
|
68
|
+
console.log(chalk.green(`Granted ${opts.permission} on ${printedPrefix} to ${granteeId}`));
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
files
|
|
76
|
+
.command("unshare <prefix>")
|
|
77
|
+
.description("Remove a file access grant")
|
|
78
|
+
.requiredOption("--with <principal>", "Email address or group id to remove")
|
|
79
|
+
.action(async (prefix: string, opts: { with: string }) => {
|
|
80
|
+
try {
|
|
81
|
+
const canonicalPrefix = normalizeFilePrefix(prefix);
|
|
82
|
+
|
|
83
|
+
const principal = opts.with;
|
|
84
|
+
const isEmail = EMAIL_PATTERN.test(principal);
|
|
85
|
+
const isGroup = GROUP_ID_PATTERN.test(principal);
|
|
86
|
+
if (!isEmail && !isGroup) {
|
|
87
|
+
console.error(chalk.red(`Invalid principal '${principal}': must be an email address or a group id matching grp_<alphanumeric>`));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const granteeType = isEmail ? "email" : "group";
|
|
91
|
+
const granteeId = isEmail ? principal.trim().toLowerCase() : principal;
|
|
92
|
+
|
|
93
|
+
const token = await ensureCognitoToken();
|
|
94
|
+
const companySlug = files.opts().company as string | undefined;
|
|
95
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
96
|
+
|
|
97
|
+
const res = await vaultApiFetch({
|
|
98
|
+
token,
|
|
99
|
+
path: `/files/${encodeURIComponent(companyUid)}/acl/revoke`,
|
|
100
|
+
method: "POST",
|
|
101
|
+
body: { prefix: canonicalPrefix, granteeType, granteeId },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!res.ok) {
|
|
105
|
+
const body = await res.json().catch(() => ({})) as Record<string, string>;
|
|
106
|
+
if (res.status === 401) {
|
|
107
|
+
console.error(chalk.red("Not authenticated — please run `hq login`"));
|
|
108
|
+
} else if (res.status === 403) {
|
|
109
|
+
console.error(chalk.red("Not authorized to modify this file prefix's ACL"));
|
|
110
|
+
} else if (res.status === 404) {
|
|
111
|
+
console.log(chalk.green(`Grant already absent for '${canonicalPrefix}' / ${granteeId}`));
|
|
112
|
+
return;
|
|
113
|
+
} else if (res.status >= 500) {
|
|
114
|
+
console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
|
|
115
|
+
} else {
|
|
116
|
+
console.error(chalk.red(body.message ?? body.error ?? "Invalid request"));
|
|
117
|
+
}
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log(chalk.green(`Removed grant for ${granteeId} on '${canonicalPrefix}'`));
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
files
|
|
129
|
+
.command("acl <prefix>")
|
|
130
|
+
.description("Show the ACL (access control list) for a file prefix")
|
|
131
|
+
.action(async (prefix: string) => {
|
|
132
|
+
try {
|
|
133
|
+
const canonicalPrefix = normalizeFilePrefix(prefix);
|
|
134
|
+
|
|
135
|
+
const token = await ensureCognitoToken();
|
|
136
|
+
const companySlug = files.opts().company as string | undefined;
|
|
137
|
+
const companyUid = await getCompanyUid(token, companySlug);
|
|
138
|
+
|
|
139
|
+
const res = await vaultApiFetch({
|
|
140
|
+
token,
|
|
141
|
+
path: `/files/${encodeURIComponent(companyUid)}/acl`,
|
|
142
|
+
query: { prefix: canonicalPrefix },
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
const body = await res.json().catch(() => ({})) as Record<string, string>;
|
|
147
|
+
if (res.status === 401) {
|
|
148
|
+
console.error(chalk.red("Not authenticated — please run `hq login`"));
|
|
149
|
+
} else if (res.status === 403) {
|
|
150
|
+
console.error(chalk.red("Not authorized to view this file prefix's ACL"));
|
|
151
|
+
} else if (res.status === 404) {
|
|
152
|
+
console.error(chalk.red(`No ACL record exists for '${canonicalPrefix}'`));
|
|
153
|
+
} else if (res.status >= 500) {
|
|
154
|
+
console.error(chalk.red(`Server error: ${body.error ?? res.statusText}`));
|
|
155
|
+
} else {
|
|
156
|
+
console.error(chalk.red(body.message ?? body.error ?? "Invalid request"));
|
|
157
|
+
}
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const data = await res.json() as {
|
|
162
|
+
acl: {
|
|
163
|
+
itemType: string;
|
|
164
|
+
companyUid: string;
|
|
165
|
+
prefix: string;
|
|
166
|
+
creatorUid: string;
|
|
167
|
+
open?: boolean;
|
|
168
|
+
entries: Array<{
|
|
169
|
+
granteeType: string;
|
|
170
|
+
granteeId: string;
|
|
171
|
+
permission: string;
|
|
172
|
+
grantedBy: string;
|
|
173
|
+
grantedAt: string;
|
|
174
|
+
}>;
|
|
175
|
+
effectivePermission?: string | null;
|
|
176
|
+
createdAt: string;
|
|
177
|
+
updatedAt: string;
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const acl = data.acl;
|
|
182
|
+
const aclStatus = acl.open ? "open" : "restricted";
|
|
183
|
+
|
|
184
|
+
console.log(chalk.green(`ACL for ${acl.prefix} (${aclStatus})`));
|
|
185
|
+
console.log(`Creator: ${acl.creatorUid}`);
|
|
186
|
+
if (acl.effectivePermission) {
|
|
187
|
+
console.log(`Your effective permission: ${acl.effectivePermission}`);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (acl.entries.length === 0) {
|
|
191
|
+
if (acl.open) {
|
|
192
|
+
console.log(chalk.gray("Open ACL — all active members have read access."));
|
|
193
|
+
} else {
|
|
194
|
+
console.log(chalk.gray("No explicit grants — only creator has access."));
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
console.log("Entries:");
|
|
200
|
+
const TYPE_W = Math.max(4, ...acl.entries.map((e) => e.granteeType.length));
|
|
201
|
+
const GRANTEE_W = Math.max(7, ...acl.entries.map((e) => e.granteeId.length));
|
|
202
|
+
const PERM_W = Math.max(10, ...acl.entries.map((e) => e.permission.length));
|
|
203
|
+
const BY_W = Math.max(10, ...acl.entries.map((e) => e.grantedBy.length));
|
|
204
|
+
const tableHeader = [
|
|
205
|
+
"TYPE".padEnd(TYPE_W),
|
|
206
|
+
"GRANTEE".padEnd(GRANTEE_W),
|
|
207
|
+
"PERMISSION".padEnd(PERM_W),
|
|
208
|
+
"GRANTED_BY".padEnd(BY_W),
|
|
209
|
+
"GRANTED_AT",
|
|
210
|
+
].join(" ");
|
|
211
|
+
console.log(chalk.bold(tableHeader));
|
|
212
|
+
for (const e of acl.entries) {
|
|
213
|
+
const grantedAt = e.grantedAt.slice(0, 10);
|
|
214
|
+
console.log([
|
|
215
|
+
e.granteeType.padEnd(TYPE_W),
|
|
216
|
+
e.granteeId.padEnd(GRANTEE_W),
|
|
217
|
+
e.permission.padEnd(PERM_W),
|
|
218
|
+
e.grantedBy.padEnd(BY_W),
|
|
219
|
+
grantedAt,
|
|
220
|
+
].join(" "));
|
|
221
|
+
}
|
|
222
|
+
} catch (err) {
|
|
223
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { registerTeamSyncCommand } from "./commands/team-sync.js";
|
|
|
24
24
|
import { registerAuthCommands } from "./commands/auth.js";
|
|
25
25
|
import { registerSecretsCommand } from "./commands/secrets.js";
|
|
26
26
|
import { registerGroupsCommand } from "./commands/groups.js";
|
|
27
|
+
import { registerFilesCommand } from "./commands/files.js";
|
|
27
28
|
|
|
28
29
|
initSentry();
|
|
29
30
|
|
|
@@ -32,7 +33,7 @@ const program = new Command();
|
|
|
32
33
|
program
|
|
33
34
|
.name("hq")
|
|
34
35
|
.description("HQ management CLI — modules, packages, and cloud sync")
|
|
35
|
-
.version("5.
|
|
36
|
+
.version("5.8.3");
|
|
36
37
|
|
|
37
38
|
// Module management subcommand group
|
|
38
39
|
const modulesCmd = program
|
|
@@ -92,6 +93,9 @@ registerSecretsCommand(program);
|
|
|
92
93
|
// Groups management (subcommand group — hq groups create|delete|add|remove|list|members)
|
|
93
94
|
registerGroupsCommand(program);
|
|
94
95
|
|
|
96
|
+
// Files ACL management (subcommand group — hq files share|unshare|acl)
|
|
97
|
+
registerFilesCommand(program);
|
|
98
|
+
|
|
95
99
|
// Onboarding (top-level — Cognito + vault-service provisioning)
|
|
96
100
|
registerOnboardCommand(program);
|
|
97
101
|
|
|
@@ -50,8 +50,7 @@ export const DEFAULT_COGNITO: CognitoAuthConfig = {
|
|
|
50
50
|
};
|
|
51
51
|
|
|
52
52
|
export const DEFAULT_VAULT_API_URL =
|
|
53
|
-
process.env.HQ_VAULT_API_URL ??
|
|
54
|
-
"https://4nfy67z28h.execute-api.us-east-1.amazonaws.com";
|
|
53
|
+
process.env.HQ_VAULT_API_URL ?? "https://hqapi.getindigo.ai";
|
|
55
54
|
|
|
56
55
|
export const DEFAULT_HQ_ROOT = path.join(os.homedir(), "hq");
|
|
57
56
|
|