@indigoai-us/hq-cli 5.6.2 → 5.7.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.
@@ -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]="a3524c87-f874-56f4-92b3-9c33467b4f95")}catch(e){}}();
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
- console.log(chalk.bold("\nHQ Sync — Push"));
35
- console.log(` HQ root: ${options.hqRoot}`);
36
- console.log(` Company: ${options.company ?? "(from .hq/config.json)"}`);
37
- console.log(` Paths: ${targetPaths.join(", ")}\n`);
38
- const accessToken = await ensureCognitoToken();
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: buildVaultConfig(accessToken),
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
- console.log(chalk.yellow(`\n⚠ Push aborted (${result.filesUploaded} uploaded, ${result.filesSkipped} skipped)`));
113
+ log(chalk.yellow(`\n⚠ Push aborted (${result.filesUploaded} uploaded, ${result.filesSkipped} skipped)`));
49
114
  process.exit(1);
50
115
  }
51
- console.log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`));
116
+ log(chalk.green(`\n✓ Pushed ${result.filesUploaded} file(s) (${formatBytes(result.bytesUploaded)}, ${result.filesSkipped} skipped)`));
52
117
  }
53
118
  catch (err) {
54
- console.error(chalk.red("\n✗ Push failed:"), err instanceof Error ? err.message : String(err));
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=a3524c87-f874-56f4-92b3-9c33467b4f95
230
+ //# debugId=5ddbce6a-fdde-506e-8cdd-2e5b0ad20fff
@@ -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]="5a05b8ed-8366-55d1-8a6a-5d145646e527")}catch(e){}}();
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]="147a783f-029d-547f-ba36-a1dbf8ed0f65")}catch(e){}}();
23
23
  import * as os from "os";
24
24
  import * as path from "path";
25
25
  import chalk from "chalk";
@@ -31,6 +31,14 @@ export const DEFAULT_COGNITO = {
31
31
  port: process.env.HQ_COGNITO_CALLBACK_PORT
32
32
  ? Number(process.env.HQ_COGNITO_CALLBACK_PORT)
33
33
  : 8765,
34
+ // Skip Cognito's Hosted UI — go straight to Google OAuth. The Cognito
35
+ // /oauth2/authorize endpoint honors `identity_provider` and performs an
36
+ // internal redirect, so the user never sees the (un-styleable) Hosted UI.
37
+ // Set HQ_COGNITO_IDENTITY_PROVIDER="" to fall back to the IdP picker for
38
+ // accounts that use email+password (admin-created users without Google).
39
+ identityProvider: process.env.HQ_COGNITO_IDENTITY_PROVIDER !== undefined
40
+ ? process.env.HQ_COGNITO_IDENTITY_PROVIDER || undefined
41
+ : "Google",
34
42
  };
35
43
  export const DEFAULT_VAULT_API_URL = process.env.HQ_VAULT_API_URL ??
36
44
  "https://4nfy67z28h.execute-api.us-east-1.amazonaws.com";
@@ -101,4 +109,4 @@ export async function refreshCachedSession() {
101
109
  }
102
110
  }
103
111
  //# sourceMappingURL=cognito-session.js.map
104
- //# debugId=5a05b8ed-8366-55d1-8a6a-5d145646e527
112
+ //# debugId=147a783f-029d-547f-ba36-a1dbf8ed0f65
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indigoai-us/hq-cli",
3
- "version": "5.6.2",
3
+ "version": "5.7.0",
4
4
  "description": "HQ by Indigo management CLI — modules and cloud sync",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -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
- console.log(chalk.bold("\nHQ Sync — Push"));
75
- console.log(` HQ root: ${options.hqRoot}`);
76
- console.log(` Company: ${options.company ?? "(from .hq/config.json)"}`);
77
- console.log(` Paths: ${targetPaths.join(", ")}\n`);
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: buildVaultConfig(accessToken),
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
- console.log(
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
- console.log(
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
- console.error(
105
- chalk.red("\n✗ Push failed:"),
106
- err instanceof Error ? err.message : String(err),
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
+ }
@@ -38,6 +38,15 @@ export const DEFAULT_COGNITO: CognitoAuthConfig = {
38
38
  port: process.env.HQ_COGNITO_CALLBACK_PORT
39
39
  ? Number(process.env.HQ_COGNITO_CALLBACK_PORT)
40
40
  : 8765,
41
+ // Skip Cognito's Hosted UI — go straight to Google OAuth. The Cognito
42
+ // /oauth2/authorize endpoint honors `identity_provider` and performs an
43
+ // internal redirect, so the user never sees the (un-styleable) Hosted UI.
44
+ // Set HQ_COGNITO_IDENTITY_PROVIDER="" to fall back to the IdP picker for
45
+ // accounts that use email+password (admin-created users without Google).
46
+ identityProvider:
47
+ process.env.HQ_COGNITO_IDENTITY_PROVIDER !== undefined
48
+ ? process.env.HQ_COGNITO_IDENTITY_PROVIDER || undefined
49
+ : "Google",
41
50
  };
42
51
 
43
52
  export const DEFAULT_VAULT_API_URL =