@indigoai-us/hq-cli 5.345.152 → 5.345.153
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 +88 -0
- package/dist/bin/hq-auth-refresh.d.ts +4 -0
- package/dist/bin/hq-auth-refresh.js +9 -1
- package/dist/command-catalog.generated.d.ts +185 -0
- package/dist/command-catalog.generated.js +227 -0
- package/dist/command-registration-plan.d.ts +6 -0
- package/dist/command-registration-plan.js +1 -0
- package/dist/commands/agent-enroll.d.ts +12 -0
- package/dist/commands/agent-enroll.js +37 -4
- package/dist/commands/core-policy.js +4 -4
- package/dist/commands/core-session-project-native.js +19 -4
- package/dist/commands/decide.d.ts +13 -0
- package/dist/commands/decide.js +74 -0
- package/dist/commands/integrations-connect.js +24 -1
- package/dist/commands/integrations-core.d.ts +14 -0
- package/dist/commands/integrations-core.js +19 -1
- package/dist/commands/integrations-import.js +22 -1
- package/dist/commands/integrations.js +43 -2
- package/dist/commands/lanes-message.js +4 -11
- package/dist/commands/lanes-questions.js +3 -0
- package/dist/commands/lanes-resume.js +4 -11
- package/dist/commands/lanes-stories.js +3 -0
- package/dist/commands/run.js +14 -5
- package/dist/lib/agent-kit/adopt-identity.d.ts +7 -1
- package/dist/lib/agent-kit/adopt-identity.js +18 -3
- package/dist/lib/agent-kit/services.d.ts +3 -1
- package/dist/lib/agent-kit/services.js +5 -1
- package/dist/lib/daemon/ops/policy.d.ts +9 -1
- package/dist/lib/daemon/ops/policy.js +37 -2
- package/dist/lib/decisions/client.d.ts +67 -0
- package/dist/lib/decisions/client.js +120 -0
- package/dist/lib/decisions/expiry.d.ts +2 -0
- package/dist/lib/decisions/expiry.js +12 -0
- package/dist/lib/decisions/fixtures/us001.d.ts +137 -0
- package/dist/lib/decisions/fixtures/us001.js +21 -0
- package/dist/lib/decisions/import-lanes.d.ts +16 -0
- package/dist/lib/decisions/import-lanes.js +152 -0
- package/dist/lib/decisions/principal.d.ts +11 -0
- package/dist/lib/decisions/principal.js +52 -0
- package/dist/lib/doctor/checks/claude-wiring.d.ts +2 -2
- package/dist/lib/doctor/checks/claude-wiring.js +21 -10
- package/dist/lib/doctor/checks/work-context.js +7 -2
- package/dist/lib/doctor/fix/apply.d.ts +3 -2
- package/dist/lib/doctor/fix/apply.js +19 -15
- package/dist/lib/doctor/fix/refresh-sync-progress.d.ts +14 -0
- package/dist/lib/doctor/fix/refresh-sync-progress.js +30 -2
- package/dist/lib/doctor/fix/remediation.js +4 -2
- package/dist/lib/doctor/hook-gate-profiles.d.ts +19 -0
- package/dist/lib/doctor/hook-gate-profiles.js +34 -0
- package/dist/lib/doctor/types.d.ts +5 -0
- package/dist/lib/install/provider-wrappers.js +5 -1
- package/dist/lib/lanes/autopilot.js +4 -1
- package/dist/lib/lanes/imported-question.d.ts +5 -0
- package/dist/lib/lanes/imported-question.js +17 -0
- package/dist/lib/lanes/questions.d.ts +0 -5
- package/dist/lib/lanes/questions.js +2 -0
- package/dist/lib/lanes/reconcile.js +21 -10
- package/dist/lib/lanes/store.d.ts +0 -8
- package/dist/lib/lanes/store.js +2 -1
- package/dist/lib/lanes/stories.d.ts +15 -0
- package/dist/lib/lanes/stories.js +25 -0
- package/dist/lib/mesh/client.d.ts +8 -0
- package/dist/lib/mesh/client.js +44 -3
- package/dist/lib/mesh/denial-codes.js +1 -1
- package/dist/lib/mesh/live/daemon/run.d.ts +9 -1
- package/dist/lib/mesh/live/daemon/run.js +30 -1
- package/dist/lib/mesh/live/flush.d.ts +7 -0
- package/dist/lib/mesh/live/flush.js +53 -10
- package/dist/lib/registry/registry.js +6 -0
- package/dist/lib/scan-packages/index.js +7 -3
- package/dist/lib/work-context/organize.d.ts +2 -0
- package/dist/lib/work-context/organize.js +22 -4
- package/dist/lib/workers-registry/index.d.ts +12 -1
- package/dist/lib/workers-registry/index.js +39 -1
- package/dist/utils/cognito-session.d.ts +4 -1
- package/dist/utils/cognito-session.js +11 -1
- package/package.json +7 -7
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import * as fs from "node:fs";
|
|
19
19
|
import * as os from "node:os";
|
|
20
|
+
import * as path from "node:path";
|
|
20
21
|
import { CLI_VERSION } from "../cli-version.js";
|
|
21
22
|
import { DEFAULT_VAULT_API_URL, personTokenCacheFile, } from "../utils/cognito-session.js";
|
|
22
23
|
import { HQ_CLIENT_NAME } from "../utils/vault-api.js";
|
|
@@ -25,7 +26,7 @@ import { queueCliLimitNoticeEngagement, queueCliLimitNoticeExposure, } from "../
|
|
|
25
26
|
import { networkTransportErrorCode } from "../utils/network-transport-error.js";
|
|
26
27
|
import { canOfferTeamUpgrade, formatUpgradeRefusal, isPlanGateError, offerUpgradeLink, planGateErrorFromPayload, } from "../utils/plan-gate-error.js";
|
|
27
28
|
import { generateHostKeyPair, machineCredsFileExists, readExternalMachineCreds, writeHostKeyPair, writeMachineCreds, } from "../lib/agent-kit/creds.js";
|
|
28
|
-
import { DEFAULT_LOCAL_AGENT_NAME, agentKitPaths, localAgentDir, } from "../lib/agent-kit/paths.js";
|
|
29
|
+
import { DEFAULT_LOCAL_AGENT_NAME, MACHINE_CREDS_NAME, agentKitPaths, agentRootDir, listLocalAgentDirs, localAgentDir, } from "../lib/agent-kit/paths.js";
|
|
29
30
|
const ENROLL_PATH = "/v1/agents/enroll";
|
|
30
31
|
/** Canonical code: un-grouped, uppercase (server hashes exactly this). */
|
|
31
32
|
export function normalizeEnrollmentCode(raw) {
|
|
@@ -88,8 +89,10 @@ export function refusalMessage(existing) {
|
|
|
88
89
|
`file is left untouched and never read.`);
|
|
89
90
|
case "machine":
|
|
90
91
|
return (`This host already has a machine identity at ${existing.file}` +
|
|
91
|
-
`${existing.agentUid ? ` (${existing.agentUid})` : ""}.
|
|
92
|
-
`
|
|
92
|
+
`${existing.agentUid ? ` (${existing.agentUid})` : ""}. To add another bot beside ` +
|
|
93
|
+
`it, re-run with --name <bot>: each bot gets its own directory and its own agent ` +
|
|
94
|
+
`identity. Re-run with --replace only to swap this agent out; the old creds and ` +
|
|
95
|
+
`host key will be overwritten.`);
|
|
93
96
|
}
|
|
94
97
|
}
|
|
95
98
|
export class EnrollError extends Error {
|
|
@@ -239,6 +242,24 @@ export function validateLocalAgentName(name) {
|
|
|
239
242
|
}
|
|
240
243
|
return trimmed;
|
|
241
244
|
}
|
|
245
|
+
/**
|
|
246
|
+
* Agent trees under `home` holding an identity, other than `self`. Existence
|
|
247
|
+
* checks only; no creds file is opened.
|
|
248
|
+
*/
|
|
249
|
+
export function otherAgentDirsOnHost(self, home = os.homedir()) {
|
|
250
|
+
const resolvedSelf = path.resolve(self);
|
|
251
|
+
const dirs = [];
|
|
252
|
+
const root = agentRootDir(home);
|
|
253
|
+
try {
|
|
254
|
+
if (fs.statSync(path.join(root, MACHINE_CREDS_NAME)).isFile())
|
|
255
|
+
dirs.push(root);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
/* no identity in the root */
|
|
259
|
+
}
|
|
260
|
+
dirs.push(...listLocalAgentDirs(home));
|
|
261
|
+
return dirs.filter((dir) => path.resolve(dir) !== resolvedSelf);
|
|
262
|
+
}
|
|
242
263
|
/** Pure-ish orchestration so tests can drive it without a TTY or network. */
|
|
243
264
|
export async function enrollHost(opts, deps = {}) {
|
|
244
265
|
const env = deps.env ?? process.env;
|
|
@@ -310,6 +331,7 @@ export async function enrollHost(opts, deps = {}) {
|
|
|
310
331
|
...(permissionsError ? { permissionsError } : {}),
|
|
311
332
|
local: target.local,
|
|
312
333
|
agentDir: paths.agentDir,
|
|
334
|
+
otherAgentDirs: otherAgentDirsOnHost(paths.agentDir, deps.home ?? paths.home),
|
|
313
335
|
agentUid: parsed.agentUid,
|
|
314
336
|
companySlug: parsed.companySlug,
|
|
315
337
|
companyUid: parsed.companyUid,
|
|
@@ -343,12 +365,23 @@ export function registerAgentEnrollCommand(agent) {
|
|
|
343
365
|
name: opts.name,
|
|
344
366
|
});
|
|
345
367
|
console.log(chalk.green(`Enrolled as ${result.agentUid} in ${result.companySlug}.`));
|
|
368
|
+
const shared = result.otherAgentDirs.length > 0;
|
|
346
369
|
if (result.local) {
|
|
347
370
|
// Say it plainly: this host belongs to a person, and the bot is a
|
|
348
371
|
// guest on it. Nothing of theirs was read, moved or overwritten.
|
|
349
372
|
console.log(chalk.dim(` This computer already has a person signed in, so this agent lives in its own\n` +
|
|
350
373
|
` directory beside them: ${result.agentDir}. Their session was not read or changed.\n` +
|
|
351
|
-
|
|
374
|
+
(shared
|
|
375
|
+
? ``
|
|
376
|
+
: ` Every hq agent command finds this tree on its own — no environment to set.`)));
|
|
377
|
+
}
|
|
378
|
+
if (shared) {
|
|
379
|
+
// Feedback ce0b9c36: a second bot that ran without this variable
|
|
380
|
+
// signed in as the first bot. Say exactly what keeps them apart.
|
|
381
|
+
console.log(chalk.yellow(` This computer has more than one agent (${result.otherAgentDirs.join(", ")}).\n` +
|
|
382
|
+
` Run this bot (its MCP server, kit install and every hq command) with\n` +
|
|
383
|
+
` HQ_AGENT_DIR=${result.agentDir}\n` +
|
|
384
|
+
` or it will act as one of the other agents. Each bot needs its own HQ_AGENT_DIR.`));
|
|
352
385
|
}
|
|
353
386
|
if (!result.permissionsEnforced) {
|
|
354
387
|
// Not a warning to bury: the credential is on disk and working,
|
|
@@ -311,7 +311,7 @@ function epochFromDate(value) {
|
|
|
311
311
|
return 0;
|
|
312
312
|
return Math.floor(parsed.getTime() / 1000);
|
|
313
313
|
}
|
|
314
|
-
function countLedgerRows(directory, withMtime, recursive, awkPath) {
|
|
314
|
+
function countLedgerRows(directory, withMtime, recursive, awkPath, platform) {
|
|
315
315
|
const counts = new Map();
|
|
316
316
|
const newest = new Map();
|
|
317
317
|
let anyFiles = false;
|
|
@@ -344,7 +344,7 @@ function countLedgerRows(directory, withMtime, recursive, awkPath) {
|
|
|
344
344
|
catch (error) {
|
|
345
345
|
const code = error.code;
|
|
346
346
|
if (withMtime && (code === 'EACCES' || code === 'EPERM')) {
|
|
347
|
-
stderr += awkCannotOpenFileDiagnostic(full, 'Permission denied', 1,
|
|
347
|
+
stderr += awkCannotOpenFileDiagnostic(full, 'Permission denied', 1, platform, awkPath);
|
|
348
348
|
continue;
|
|
349
349
|
}
|
|
350
350
|
if (expectedFsMiss(error))
|
|
@@ -449,8 +449,8 @@ export function runPolicyAgeReport(args, root, env = process.env, nowEpoch = Mat
|
|
|
449
449
|
const ledgerDir = env.HQ_POLICY_LEDGER_DIR || path.join(root, 'workspace/orchestrator/policy-trigger-state');
|
|
450
450
|
const retrievalDir = env.HQ_POLICY_RETRIEVAL_LEDGER_DIR || path.join(root, 'workspace/orchestrator/policy-retrieval-state');
|
|
451
451
|
const awkPath = env.PATH ?? '/usr/bin:/bin';
|
|
452
|
-
const usage = countLedgerRows(ledgerDir, true, true, awkPath);
|
|
453
|
-
const retrieval = countLedgerRows(retrievalDir, false, false, awkPath);
|
|
452
|
+
const usage = countLedgerRows(ledgerDir, true, true, awkPath, platform);
|
|
453
|
+
const retrieval = countLedgerRows(retrievalDir, false, false, awkPath, platform);
|
|
454
454
|
const dirs = parsed.dirs.length > 0 ? parsed.dirs : [path.join(root, 'personal/policies'), path.join(root, 'core/policies')];
|
|
455
455
|
const rows = [];
|
|
456
456
|
let stderr = usage.stderr + retrieval.stderr;
|
|
@@ -249,6 +249,13 @@ export function runSessionProjectFast(argv, stdin, cwd, root, dependencies = {})
|
|
|
249
249
|
return error?.code !== "ESRCH";
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
|
+
// Windows keeps a just-released lock directory or pid file in a delete-pending
|
|
253
|
+
// state while another waiter still holds a handle on it. Creating the same
|
|
254
|
+
// name in that window fails with EPERM, EACCES or EBUSY instead of EEXIST, so
|
|
255
|
+
// it is lock contention and the waiter keeps polling until its deadline.
|
|
256
|
+
function isTransientWindowsLockError(error) {
|
|
257
|
+
return process.platform === "win32" && ["EPERM", "EACCES", "EBUSY"].includes(error?.code);
|
|
258
|
+
}
|
|
252
259
|
function acquireProjectWriteLock(prdPath) {
|
|
253
260
|
const lock = `${prdPath}.write-lock`;
|
|
254
261
|
const ownerFile = path.join(lock, "pid");
|
|
@@ -260,14 +267,21 @@ export function runSessionProjectFast(argv, stdin, cwd, root, dependencies = {})
|
|
|
260
267
|
try {
|
|
261
268
|
fs.writeFileSync(ownerFile, `${process.pid}\n`, { flag: "wx" });
|
|
262
269
|
}
|
|
263
|
-
catch {
|
|
264
|
-
fs.rmSync(lock, { recursive: true, force: true });
|
|
265
|
-
|
|
270
|
+
catch (error) {
|
|
271
|
+
fs.rmSync(lock, { recursive: true, force: true, maxRetries: 10, retryDelay: 10 });
|
|
272
|
+
if (!isTransientWindowsLockError(error))
|
|
273
|
+
return undefined;
|
|
274
|
+
sleepSync(20);
|
|
275
|
+
continue;
|
|
266
276
|
}
|
|
267
277
|
process.once("exit", () => releaseProjectWriteLock(lock));
|
|
268
278
|
return lock;
|
|
269
279
|
}
|
|
270
280
|
catch (error) {
|
|
281
|
+
if (isTransientWindowsLockError(error)) {
|
|
282
|
+
sleepSync(20);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
271
285
|
if (error?.code !== "EEXIST")
|
|
272
286
|
return undefined;
|
|
273
287
|
let owner = 0;
|
|
@@ -299,8 +313,9 @@ export function runSessionProjectFast(argv, stdin, cwd, root, dependencies = {})
|
|
|
299
313
|
function releaseProjectWriteLock(lock) {
|
|
300
314
|
try {
|
|
301
315
|
const owner = Number.parseInt(fs.readFileSync(path.join(lock, "pid"), "utf8"), 10);
|
|
316
|
+
// Retry so a waiter's open handle on the pid file cannot leave an ownerless lock directory behind.
|
|
302
317
|
if (owner === process.pid)
|
|
303
|
-
fs.rmSync(lock, { recursive: true, force: true });
|
|
318
|
+
fs.rmSync(lock, { recursive: true, force: true, maxRetries: 10, retryDelay: 10 });
|
|
304
319
|
}
|
|
305
320
|
catch { /* lock cleanup is best-effort; stale owners are reclaimed */ }
|
|
306
321
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { DecisionsClient } from "../lib/decisions/client.js";
|
|
3
|
+
export interface DecideDeps {
|
|
4
|
+
client: DecisionsClient;
|
|
5
|
+
audience: () => Promise<string>;
|
|
6
|
+
importAudience: () => Promise<string>;
|
|
7
|
+
root: () => string;
|
|
8
|
+
company: (ref?: string) => Promise<string>;
|
|
9
|
+
now: () => number;
|
|
10
|
+
print: (value: string) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function registerDecideCommand(program: Command, overrides?: Partial<DecideDeps>): void;
|
|
13
|
+
//# sourceMappingURL=decide.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { getCompanyUid } from "../utils/vault-api.js";
|
|
2
|
+
import { resolveActiveCompany } from "./workers.js";
|
|
3
|
+
import { decisionExpiry } from "../lib/decisions/expiry.js";
|
|
4
|
+
import { Option } from "commander";
|
|
5
|
+
import { ensureCognitoToken, resolveDefaultHqRoot } from "../utils/cognito-session.js";
|
|
6
|
+
import { DecisionsClient, DecisionError, parseOptions } from "../lib/decisions/client.js";
|
|
7
|
+
import { askAudience, importAudience } from "../lib/decisions/principal.js";
|
|
8
|
+
import { importLaneQuestions } from "../lib/decisions/import-lanes.js";
|
|
9
|
+
export function registerDecideCommand(program, overrides = {}) {
|
|
10
|
+
const token = () => ensureCognitoToken({ interactive: false });
|
|
11
|
+
const deps = {
|
|
12
|
+
client: new DecisionsClient(token), audience: async () => askAudience(await token()),
|
|
13
|
+
importAudience: async () => importAudience(await token()),
|
|
14
|
+
company: async (ref) => getCompanyUid(await token(), ref ?? resolveActiveCompany(deps.root())),
|
|
15
|
+
now: () => Date.now(),
|
|
16
|
+
root: () => resolveDefaultHqRoot(), print: value => console.log(value), ...overrides,
|
|
17
|
+
};
|
|
18
|
+
const decide = program.command("decide").description("Ask and answer owner decisions in the HQ Queue");
|
|
19
|
+
const repeat = (value, previous) => [...previous, value];
|
|
20
|
+
decide.command("ask").description("Create an option-choice owner decision as the personal local bot").argument("<statement>")
|
|
21
|
+
.addOption(new Option("--to <audience>", "Decision audience").choices(["me"]).default("me"))
|
|
22
|
+
.option("--option <id|label>", "Choice, repeatable", repeat, [])
|
|
23
|
+
.option("--recommend <id>", "Recommended option id")
|
|
24
|
+
.option("--context-link <url>", "Context link, repeatable", repeat, [])
|
|
25
|
+
.option("--context <text>", "Short context brief", "")
|
|
26
|
+
.option("--company <slug|uid>", "Company (defaults to the active company)")
|
|
27
|
+
.option("--expires <duration|iso>", "Expiry duration (m, h, d) or ISO date-time", "72h")
|
|
28
|
+
.option("--commitment <id>", "Related commitment")
|
|
29
|
+
.option("--json", "Print the decision record")
|
|
30
|
+
.action(async (statement, opts) => {
|
|
31
|
+
if (!statement.trim())
|
|
32
|
+
throw new DecisionError("invalid_statement", "Statement must not be empty");
|
|
33
|
+
const options = parseOptions(opts.option, opts.recommend);
|
|
34
|
+
for (const link of opts.contextLink) {
|
|
35
|
+
let valid = false;
|
|
36
|
+
try {
|
|
37
|
+
valid = ["https:", "http:"].includes(new URL(link).protocol);
|
|
38
|
+
}
|
|
39
|
+
catch { /* invalid URL */ }
|
|
40
|
+
if (!valid)
|
|
41
|
+
throw new DecisionError("invalid_context_link", "Context links must be HTTP(S) URLs");
|
|
42
|
+
}
|
|
43
|
+
const expiresAt = decisionExpiry(opts.expires, deps.now());
|
|
44
|
+
const owner = await deps.audience();
|
|
45
|
+
const companyUid = await deps.company(opts.company);
|
|
46
|
+
const record = await deps.client.ask({ companyUid, expiresAt, audience: { kind: "person", personUid: owner }, statement, options, context: {
|
|
47
|
+
links: opts.contextLink, brief: opts.context, commitmentId: opts.commitment ?? null,
|
|
48
|
+
} });
|
|
49
|
+
deps.print(opts.json ? JSON.stringify(record, null, 2) : record.id);
|
|
50
|
+
});
|
|
51
|
+
decide.command("list").description("Read your decisions across companies, oldest first").option("--open", "List open decisions").option("--json", "Print JSON")
|
|
52
|
+
.action(async (opts) => {
|
|
53
|
+
const decisions = await deps.client.list(opts.open);
|
|
54
|
+
deps.print(opts.json ? JSON.stringify(decisions, null, 2) : decisions.map(record => `${record.id}\t${record.state}\t${record.statement}`).join("\n"));
|
|
55
|
+
});
|
|
56
|
+
decide.command("show").description("Show a visible decision, including its options and answer").argument("<id>").action(async (id) => deps.print(JSON.stringify(await deps.client.show(id), null, 2)));
|
|
57
|
+
decide.command("answer").description("Answer with an option ID; the server enforces expiry and first answer").argument("<id>").requiredOption("--choice <id>", "Option id")
|
|
58
|
+
.addOption(new Option("--source <source>", "Answer surface").choices(["desktop", "thread"]).default("desktop"))
|
|
59
|
+
.action(async (id, opts) => {
|
|
60
|
+
if (!opts.choice.trim())
|
|
61
|
+
throw new DecisionError("invalid_choice", "Choice must not be empty");
|
|
62
|
+
deps.print((await deps.client.answer(id, opts.choice, opts.source)).state);
|
|
63
|
+
});
|
|
64
|
+
for (const action of ["talk", "withdraw"]) {
|
|
65
|
+
decide.command(action).description(action === "talk" ? "Open the decision's bot conversation" : "Withdraw a decision you asked").argument("<id>").action(async (id) => deps.print((await deps.client.transition(id, action)).state));
|
|
66
|
+
}
|
|
67
|
+
decide.command("import-lanes").description("Import option-style legacy questions once; report skipped advanced questions")
|
|
68
|
+
.option("--json", "Print import counts and skipped question reasons")
|
|
69
|
+
.action(async (opts) => {
|
|
70
|
+
const counts = await importLaneQuestions(deps.root(), await deps.importAudience(), deps.client);
|
|
71
|
+
deps.print(opts.json ? JSON.stringify(counts, null, 2) : `Imported ${counts.imported}; already imported ${counts.alreadyImported}; skipped ${counts.skipped}`);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=decide.js.map
|
|
@@ -1228,6 +1228,18 @@ export function findAlreadyConnected(connections, app) {
|
|
|
1228
1228
|
return wantSlug !== "" && displayNameSlug(displayName) === wantSlug;
|
|
1229
1229
|
});
|
|
1230
1230
|
}
|
|
1231
|
+
/**
|
|
1232
|
+
* True when a member (not an owner or admin) is looking at a connection another
|
|
1233
|
+
* person created. An unknown viewer or creator is never treated as someone
|
|
1234
|
+
* else's, so older servers keep the ordinary hints.
|
|
1235
|
+
*/
|
|
1236
|
+
function connectedBySomeoneElse(connection, viewer) {
|
|
1237
|
+
if (!viewer?.personUid || !connection.createdBy)
|
|
1238
|
+
return false;
|
|
1239
|
+
if (viewer.role === "owner" || viewer.role === "admin")
|
|
1240
|
+
return false;
|
|
1241
|
+
return connection.createdBy !== viewer.personUid;
|
|
1242
|
+
}
|
|
1231
1243
|
/**
|
|
1232
1244
|
* Report an app as already connected instead of opening a second sign-in.
|
|
1233
1245
|
* Returns true when the connect should stop here.
|
|
@@ -1239,8 +1251,11 @@ async function refuseIfAlreadyConnected(token, companyUid, app, opts) {
|
|
|
1239
1251
|
if (!app || opts.force || opts.mcpUrl || opts.docsUrl || opts.entryId)
|
|
1240
1252
|
return false;
|
|
1241
1253
|
let connections;
|
|
1254
|
+
let viewer;
|
|
1242
1255
|
try {
|
|
1243
|
-
|
|
1256
|
+
const surface = await fetchAdminSurface(token, companyUid);
|
|
1257
|
+
connections = surface.connections ?? [];
|
|
1258
|
+
viewer = surface.viewer;
|
|
1244
1259
|
}
|
|
1245
1260
|
catch {
|
|
1246
1261
|
// Never let this convenience check block a connect. If the list cannot be
|
|
@@ -1263,6 +1278,14 @@ async function refuseIfAlreadyConnected(token, companyUid, app, opts) {
|
|
|
1263
1278
|
}
|
|
1264
1279
|
const label = redactUrlsInText(existing.installation?.displayName ?? slug);
|
|
1265
1280
|
console.log(chalk.green(`${label} is already connected.`));
|
|
1281
|
+
if (connectedBySomeoneElse(existing, viewer)) {
|
|
1282
|
+
// Members may add apps and reconnect their own, but replacing a connection
|
|
1283
|
+
// someone else created stays with an owner or admin: the server refuses
|
|
1284
|
+
// both the reconnect and the --force re-add with INTEGRATION_RECONNECT_FORBIDDEN.
|
|
1285
|
+
console.log(chalk.dim(" Someone else connected it. To sign it in again, ask a company owner or admin to run:\n" +
|
|
1286
|
+
` hq integrations reconnect --provider ${slug}`));
|
|
1287
|
+
return true;
|
|
1288
|
+
}
|
|
1266
1289
|
console.log(chalk.dim(` Sign in again: hq integrations reconnect --provider ${slug}\n` +
|
|
1267
1290
|
` Add a second: hq integrations connect ${shellQuote(app)} --force`));
|
|
1268
1291
|
return true;
|
|
@@ -104,6 +104,12 @@ export interface AdminSurface {
|
|
|
104
104
|
role: string;
|
|
105
105
|
canManageGovernance: boolean;
|
|
106
106
|
canManageIntegrations?: boolean;
|
|
107
|
+
/**
|
|
108
|
+
* Owners, admins, and members may add apps; guests may not. Removing and
|
|
109
|
+
* governing apps stays with `canManageIntegrations` / `canManageGovernance`.
|
|
110
|
+
* Absent on servers that predate members connecting apps.
|
|
111
|
+
*/
|
|
112
|
+
canAddIntegrations?: boolean;
|
|
107
113
|
};
|
|
108
114
|
connections: AdminConnection[];
|
|
109
115
|
audit: AdminAuditEvent[];
|
|
@@ -207,6 +213,14 @@ export declare function upstreamUnavailableMessage(status: number): string;
|
|
|
207
213
|
*/
|
|
208
214
|
export declare function raiseIfUpstreamUnavailable(res: Response): void;
|
|
209
215
|
export declare function raiseIfUnauthorized(res: Response): void;
|
|
216
|
+
/**
|
|
217
|
+
* A member connected, reinstalled, or signed in to an app that someone else
|
|
218
|
+
* already connected for the company. Members may add apps and reconnect their
|
|
219
|
+
* own, but replacing another person's company connection stays with an owner
|
|
220
|
+
* or admin, so the server refuses it with this 403.
|
|
221
|
+
*/
|
|
222
|
+
export declare const INTEGRATION_RECONNECT_FORBIDDEN_CODE = "INTEGRATION_RECONNECT_FORBIDDEN";
|
|
223
|
+
export declare const INTEGRATION_RECONNECT_FORBIDDEN_MESSAGE: string;
|
|
210
224
|
/**
|
|
211
225
|
* Single non-2xx funnel for the REST (non-JSON-RPC) integration routes. Applies
|
|
212
226
|
* the auth and upstream-availability guards in order, then raises the server's
|
|
@@ -259,6 +259,23 @@ export function raiseIfUnauthorized(res) {
|
|
|
259
259
|
if (res.status === 401)
|
|
260
260
|
throw new AuthError();
|
|
261
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* A member connected, reinstalled, or signed in to an app that someone else
|
|
264
|
+
* already connected for the company. Members may add apps and reconnect their
|
|
265
|
+
* own, but replacing another person's company connection stays with an owner
|
|
266
|
+
* or admin, so the server refuses it with this 403.
|
|
267
|
+
*/
|
|
268
|
+
export const INTEGRATION_RECONNECT_FORBIDDEN_CODE = "INTEGRATION_RECONNECT_FORBIDDEN";
|
|
269
|
+
export const INTEGRATION_RECONNECT_FORBIDDEN_MESSAGE = "This app is already connected for the company by someone else, so you can't reconnect or replace it. " +
|
|
270
|
+
"Ask a company owner or admin to reconnect it, or ask them to share the existing connection with you " +
|
|
271
|
+
"(`hq integrations list` shows the apps you can use).";
|
|
272
|
+
/**
|
|
273
|
+
* CLI wording for server error codes whose response text alone does not tell
|
|
274
|
+
* the person what to run next. Unlisted codes keep the server's own text.
|
|
275
|
+
*/
|
|
276
|
+
const CLI_ERROR_MESSAGES = new Map([
|
|
277
|
+
[INTEGRATION_RECONNECT_FORBIDDEN_CODE, INTEGRATION_RECONNECT_FORBIDDEN_MESSAGE],
|
|
278
|
+
]);
|
|
262
279
|
/**
|
|
263
280
|
* Single non-2xx funnel for the REST (non-JSON-RPC) integration routes. Applies
|
|
264
281
|
* the auth and upstream-availability guards in order, then raises the server's
|
|
@@ -279,7 +296,8 @@ export async function raiseForResponse(res, fallback) {
|
|
|
279
296
|
// hq-pro mints this error text, so it is untrusted input to a message we
|
|
280
297
|
// print verbatim — scrub credentials out before it can reach a terminal or a
|
|
281
298
|
// Sentry crash report (same reasoning as the gateway path below).
|
|
282
|
-
const detail =
|
|
299
|
+
const detail = (body.code ? CLI_ERROR_MESSAGES.get(body.code) : undefined) ??
|
|
300
|
+
redactErrorText(body.error ?? "");
|
|
283
301
|
const redirectUri = httpsUrlOrNull(body.redirectUri);
|
|
284
302
|
throw new IntegrationsCliError(detail || `${fallback} (HTTP ${res.status})`, {
|
|
285
303
|
expected: isClientError(res.status),
|
|
@@ -11,7 +11,7 @@ import chalk from "chalk";
|
|
|
11
11
|
import { ensureCognitoIdToken, ensureCognitoToken, resolveDefaultHqRoot } from "../utils/cognito-session.js";
|
|
12
12
|
import { getCompanyUid, vaultApiFetch } from "../utils/vault-api.js";
|
|
13
13
|
import { redactSensitiveUrl, redactUrlsInText } from "../utils/redact-url.js";
|
|
14
|
-
import { bareProvider, IntegrationsCliError, printJson } from "./integrations-core.js";
|
|
14
|
+
import { bareProvider, INTEGRATION_RECONNECT_FORBIDDEN_CODE, IntegrationsCliError, printJson, } from "./integrations-core.js";
|
|
15
15
|
import { installIntegration, startOAuth } from "./integrations-api.js";
|
|
16
16
|
import { loadRevealedSecrets } from "./secrets.js";
|
|
17
17
|
import { registerServer } from "./mcp-registration.js";
|
|
@@ -147,6 +147,9 @@ function oauthRequired(error) {
|
|
|
147
147
|
return error instanceof IntegrationsCliError &&
|
|
148
148
|
(error.code === OAUTH_REQUIRED_CODE || error.oauthProtected === true);
|
|
149
149
|
}
|
|
150
|
+
function reconnectForbidden(error) {
|
|
151
|
+
return error instanceof IntegrationsCliError && error.code === INTEGRATION_RECONNECT_FORBIDDEN_CODE;
|
|
152
|
+
}
|
|
150
153
|
function reconnectCommand(name, provider) {
|
|
151
154
|
return `hq integrations reconnect ${name} --provider ${bareProvider(provider)}`;
|
|
152
155
|
}
|
|
@@ -190,6 +193,24 @@ async function importRemote(token, companyUid, name, entry, dryRun) {
|
|
|
190
193
|
if (dryRun)
|
|
191
194
|
return { name, status: "imported", reason: "would connect remote MCP endpoint", provider };
|
|
192
195
|
const credential = staticCredential(entry);
|
|
196
|
+
try {
|
|
197
|
+
return await installRemote(token, companyUid, name, provider, entry, credential);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
// Members may import connectors, but not over an app someone else already
|
|
201
|
+
// connected for the company. Skip that one connector and keep importing
|
|
202
|
+
// the rest rather than aborting the whole run.
|
|
203
|
+
if (!reconnectForbidden(error))
|
|
204
|
+
throw error;
|
|
205
|
+
return {
|
|
206
|
+
name,
|
|
207
|
+
status: "skipped",
|
|
208
|
+
provider,
|
|
209
|
+
reason: "already connected by someone else; ask a company owner or admin to reconnect it",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async function installRemote(token, companyUid, name, provider, entry, credential) {
|
|
193
214
|
try {
|
|
194
215
|
const result = await installIntegration(token, companyUid, {
|
|
195
216
|
mcpUrl: entry.url,
|
|
@@ -297,6 +297,31 @@ export async function waitForApprovalTerminal(token, companyUid, queueId, option
|
|
|
297
297
|
}
|
|
298
298
|
return null;
|
|
299
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Read the stored result of an approved call that already ran. The confirm
|
|
302
|
+
* route answers an approve for a `resumed` row from the queue row
|
|
303
|
+
* (alreadyResumed) without calling the provider again. Returns undefined when
|
|
304
|
+
* the result cannot be read; that never fails the approve.
|
|
305
|
+
*/
|
|
306
|
+
async function fetchStoredApprovalResult(token, companyUid, connectionId, queueId) {
|
|
307
|
+
try {
|
|
308
|
+
const res = await vaultApiFetch({
|
|
309
|
+
token,
|
|
310
|
+
path: `/v1/integrations/confirm/${encodeURIComponent(queueId)}/approve`,
|
|
311
|
+
method: "POST",
|
|
312
|
+
body: { companyUid, connectionId },
|
|
313
|
+
});
|
|
314
|
+
if (!res.ok)
|
|
315
|
+
return undefined;
|
|
316
|
+
const body = (await res.json().catch(() => ({})));
|
|
317
|
+
if (body.status !== "resumed" || body.alreadyResumed !== true)
|
|
318
|
+
return undefined;
|
|
319
|
+
return { result: body.result };
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return undefined;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
300
325
|
function isPendingApprovalResponse(status) {
|
|
301
326
|
return ["pending", "approved", "approved_pending_run", "in_progress"].includes(status ?? "");
|
|
302
327
|
}
|
|
@@ -563,8 +588,18 @@ export function registerIntegrationsCommand(program) {
|
|
|
563
588
|
}
|
|
564
589
|
return;
|
|
565
590
|
}
|
|
566
|
-
|
|
567
|
-
|
|
591
|
+
// feedback_3bf0f73f-1537-4d32-a143-9dfc43bc293f: the async worker
|
|
592
|
+
// stores the provider result on the queue row, but the history
|
|
593
|
+
// rows this poll reads carry no result. Re-posting approve for a
|
|
594
|
+
// `resumed` row does not run the call again: the gateway answers
|
|
595
|
+
// it from the stored row with alreadyResumed and the cached result.
|
|
596
|
+
const ran = terminal.status === "resumed";
|
|
597
|
+
const stored = ran
|
|
598
|
+
? await fetchStoredApprovalResult(token, companyUid, connection.id, queueId)
|
|
599
|
+
: undefined;
|
|
600
|
+
if (opts.json) {
|
|
601
|
+
printJson(stored && stored.result !== undefined ? { ...terminal, result: stored.result } : terminal);
|
|
602
|
+
}
|
|
568
603
|
else if (approvalOutcomeFailed(terminal)) {
|
|
569
604
|
const unknown = terminal.status === "write_unknown" || terminal.outcome === "unknown";
|
|
570
605
|
console.log(chalk.red(unknown
|
|
@@ -573,6 +608,12 @@ export function registerIntegrationsCommand(program) {
|
|
|
573
608
|
}
|
|
574
609
|
else if (terminal.status === "resumed" || terminal.status === "completed" || terminal.outcome === "ran") {
|
|
575
610
|
console.log(chalk.green("Approved — the call completed."));
|
|
611
|
+
if (stored && stored.result !== undefined && stored.result !== null) {
|
|
612
|
+
printJson(unwrapGatewayResult(stored.result));
|
|
613
|
+
}
|
|
614
|
+
else if (ran) {
|
|
615
|
+
console.log(chalk.yellow(`The call's result could not be read back. Run \`hq integrations approve ${queueId}\` again to fetch it; the call will not run twice.`));
|
|
616
|
+
}
|
|
576
617
|
}
|
|
577
618
|
else {
|
|
578
619
|
console.log(chalk.yellow(`Approval ended with status ${terminal.status ?? terminal.outcome ?? "unknown"}.`));
|
|
@@ -8,7 +8,7 @@ import { parseOutpostHireGrantContext, sameHireGrantSenior } from "../lib/lanes/
|
|
|
8
8
|
import { dispatchLaneDelivery } from "../lib/lanes/delivery-core.js";
|
|
9
9
|
import { dropboxInboxHasMessages, drainDropbox, formatDropboxMessages, formatMonitorEvent, requeueDropboxMessages } from "../lib/lanes/dropbox.js";
|
|
10
10
|
import { isPidAlive, runWatcherLoop, WATCHER_OUT_MAX_BYTES, watcherOutLogPath, writeWatcherDecision } from "../lib/lanes/watcher.js";
|
|
11
|
-
import { laneStories,
|
|
11
|
+
import { attributedStoryRow, laneStories, storiesAfterAttribution, storyWarning } from "../lib/lanes/stories.js";
|
|
12
12
|
import { artifactIdentityKeys, rollbackStorySwitchAttribution, storySwitchNotice } from "../lib/lanes/story-switch.js";
|
|
13
13
|
import { laneRunDir, probePidRecord, readPidRecord, readProcStarttime, spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
14
14
|
import { findRetiredLaneEntry, keepAliveExclusion, keepAliveSupersededBy, LANE_RUNNING_SUPERSEDED_MESSAGE_DELIVERY_FLAG, LANE_SUPERSESSION_LINEAGE_AWARE_FLAG, loadRetiredLaneIds } from "../lib/lanes/keep-alive.js";
|
|
@@ -630,15 +630,13 @@ export function createLaneMessageHandlers(deps) {
|
|
|
630
630
|
if (attributionChanged) {
|
|
631
631
|
const already = current.attribution_history.some((entry) => entry.message_id === messageId);
|
|
632
632
|
if (!already) {
|
|
633
|
-
const
|
|
633
|
+
const addedStory = attributedStoryRow(current, projectId, storyId, at);
|
|
634
634
|
storySwitchRollback = {
|
|
635
635
|
message_id: messageId,
|
|
636
636
|
project_id: current.project_id,
|
|
637
637
|
story_id: current.story_id,
|
|
638
638
|
no_story_reason: current.no_story_reason,
|
|
639
|
-
...(
|
|
640
|
-
added_story: { project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
|
|
641
|
-
} : {}),
|
|
639
|
+
...(addedStory ? { added_story: addedStory } : {}),
|
|
642
640
|
story_switch_unacknowledged: current.story_switch_unacknowledged,
|
|
643
641
|
};
|
|
644
642
|
next = {
|
|
@@ -646,12 +644,7 @@ export function createLaneMessageHandlers(deps) {
|
|
|
646
644
|
project_id: projectId,
|
|
647
645
|
story_id: storyId,
|
|
648
646
|
no_story_reason: storyId ? undefined : current.no_story_reason,
|
|
649
|
-
stories:
|
|
650
|
-
? laneStories(current)
|
|
651
|
-
: [
|
|
652
|
-
...laneStories(current),
|
|
653
|
-
{ project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
|
|
654
|
-
],
|
|
647
|
+
stories: storiesAfterAttribution(current, projectId, storyId, at),
|
|
655
648
|
attribution_history: appendCappedHistory(current.attribution_history, {
|
|
656
649
|
project_id: projectId,
|
|
657
650
|
story_id: storyId,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { isQuestionImported } from "../lib/lanes/imported-question.js";
|
|
2
3
|
import { buildOutpostQuestionAnswerCommand, buildOutpostQuestionCancelCommand, buildOutpostQuestionShowCommand, dispatchErrorMessage } from "../lib/lanes/outpost-dispatch.js";
|
|
3
4
|
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
4
5
|
import { sessionIdFromEnv } from "../lib/mesh/live/session-identity.js";
|
|
@@ -639,6 +640,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
639
640
|
const visibleQuestion = deliveredLocalQuestions.get(q.question_id) ?? q;
|
|
640
641
|
const row = {
|
|
641
642
|
...visibleQuestion,
|
|
643
|
+
...(isQuestionImported(hqRoot, q.question_id) ? { status_display: "moved to hq decide", moved_to: "hq decide" } : {}),
|
|
642
644
|
priority: visibleQuestion.priority ?? null,
|
|
643
645
|
priority_display: visibleQuestion.priority ?? "-",
|
|
644
646
|
audience: resolveAudience(hqRoot, visibleQuestion, ctx),
|
|
@@ -741,6 +743,7 @@ export function createLaneQuestionsHandlers(deps) {
|
|
|
741
743
|
ok: true,
|
|
742
744
|
action: "questions.show",
|
|
743
745
|
question,
|
|
746
|
+
...(isQuestionImported(hqRoot, question.question_id) ? { status_display: "moved to hq decide", moved_to: "hq decide" } : {}),
|
|
744
747
|
audience: resolveAudience(hqRoot, question, ctx),
|
|
745
748
|
body,
|
|
746
749
|
});
|
|
@@ -4,7 +4,7 @@ import { appendCappedHistory, isFailedQuotaKeepAliveLane, providerSessionPortabl
|
|
|
4
4
|
import { parseCliEffort } from "../lib/lanes/effort.js";
|
|
5
5
|
import { parseOutpostHireGrantContext } from "../lib/lanes/hire-grant-dispatch.js";
|
|
6
6
|
import { assertNoSecretText } from "../lib/lanes/text-scan.js";
|
|
7
|
-
import { laneStories,
|
|
7
|
+
import { attributedStoryRow, laneStories, storiesAfterAttribution, storyWarning } from "../lib/lanes/stories.js";
|
|
8
8
|
import { artifactIdentityKeys, rollbackStorySwitchAttribution, storySwitchNotice } from "../lib/lanes/story-switch.js";
|
|
9
9
|
import { markSpawnFailure, spawnLaneWorker } from "../lib/lanes/spawn.js";
|
|
10
10
|
import { appendProjectFocusOverride, removeProjectFocusOverride } from "../lib/lanes/story-ledger.js";
|
|
@@ -359,15 +359,13 @@ export function createLaneResumeHandlers(deps) {
|
|
|
359
359
|
const already = current.attribution_history.some((entry) => entry.message_id === messageId);
|
|
360
360
|
if (already)
|
|
361
361
|
return current;
|
|
362
|
-
const
|
|
362
|
+
const addedStory = attributedStoryRow(current, projectId, storyId, at);
|
|
363
363
|
storySwitchRollback = {
|
|
364
364
|
message_id: messageId,
|
|
365
365
|
project_id: current.project_id,
|
|
366
366
|
story_id: current.story_id,
|
|
367
367
|
no_story_reason: current.no_story_reason,
|
|
368
|
-
...(
|
|
369
|
-
added_story: { project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
|
|
370
|
-
} : {}),
|
|
368
|
+
...(addedStory ? { added_story: addedStory } : {}),
|
|
371
369
|
story_switch_unacknowledged: current.story_switch_unacknowledged,
|
|
372
370
|
};
|
|
373
371
|
return {
|
|
@@ -375,12 +373,7 @@ export function createLaneResumeHandlers(deps) {
|
|
|
375
373
|
project_id: projectId,
|
|
376
374
|
story_id: storyId,
|
|
377
375
|
no_story_reason: storyId ? undefined : current.no_story_reason,
|
|
378
|
-
stories:
|
|
379
|
-
? laneStories(current)
|
|
380
|
-
: [
|
|
381
|
-
...laneStories(current),
|
|
382
|
-
{ project_id: projectId, story_id: storyId, status: "in_progress", added_at: at, updated_at: at },
|
|
383
|
-
],
|
|
376
|
+
stories: storiesAfterAttribution(current, projectId, storyId, at),
|
|
384
377
|
attribution_history: appendCappedHistory(current.attribution_history, {
|
|
385
378
|
project_id: projectId,
|
|
386
379
|
story_id: storyId,
|
|
@@ -44,6 +44,9 @@ export function createLaneStoryHandlers(deps) {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
function addCarriedStory(lane, projectId, storyId, at) {
|
|
47
|
+
if (!storyId.trim()) {
|
|
48
|
+
throw new LanesError("missing_story", "A story id is required to add a story to a lane", { lane_id: lane.lane_id });
|
|
49
|
+
}
|
|
47
50
|
if (laneStory(lane, projectId, storyId)) {
|
|
48
51
|
throw new LanesError("story_already_on_lane", `Story ${projectId}/${storyId} is already carried by lane ${lane.lane_id}`, {
|
|
49
52
|
lane_id: lane.lane_id,
|
package/dist/commands/run.js
CHANGED
|
@@ -149,11 +149,20 @@ export function registerRunCommand(program) {
|
|
|
149
149
|
// error instead of hanging on varlock's never-settling promise, in case
|
|
150
150
|
// the shape-dependent guard above ever silently degrades.
|
|
151
151
|
await resolveEnvValuesOrThrow(graph);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
152
|
+
// varlock's item.errors includes warning-severity entries (isWarning),
|
|
153
|
+
// e.g. "Value is very short! Is it actually sensitive?" for a short
|
|
154
|
+
// sensitive value. Print those and carry on; only real errors are fatal.
|
|
155
|
+
const schemaItems = Object.entries(graph.configSchema);
|
|
156
|
+
const itemMessages = (wantWarnings) => schemaItems.flatMap(([k, item]) => (item.errors ?? [])
|
|
157
|
+
.filter((e) => Boolean(e?.isWarning) === wantWarnings)
|
|
158
|
+
.map((e) => ` ${k}: ${e?.message ?? String(e)}`));
|
|
159
|
+
const warningMsgs = itemMessages(true);
|
|
160
|
+
if (warningMsgs.length > 0) {
|
|
161
|
+
process.stderr.write(`Warning: env var checks reported warnings:\n${warningMsgs.join('\n')}\n`);
|
|
162
|
+
}
|
|
163
|
+
const errorMsgs = itemMessages(false);
|
|
164
|
+
if (errorMsgs.length > 0) {
|
|
165
|
+
process.stderr.write(`Error: failed to resolve env vars:\n${errorMsgs.join('\n')}\n`);
|
|
157
166
|
process.exit(1);
|
|
158
167
|
}
|
|
159
168
|
const resolvedEnv = graph.getResolvedEnvObject();
|