@indigoai-us/hq-cli 5.345.47 → 5.345.48
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 +5 -0
- package/assets/scaffold/core/scripts/hq-session.sh +113 -7
- package/assets/scaffold/core/scripts/lib/session-scope-capability.sh +101 -0
- package/dist/command-catalog.generated.d.ts +4 -0
- package/dist/command-catalog.generated.js +5 -0
- package/dist/commands/core-hq-session-native.d.ts +2 -0
- package/dist/commands/core-hq-session-native.js +242 -52
- package/dist/commands/core-resolve-company-native.d.ts +1 -0
- package/dist/commands/core-resolve-company-native.js +41 -11
- package/dist/commands/secrets.d.ts +5 -0
- package/dist/commands/secrets.js +181 -2
- package/dist/commands/workers.d.ts +4 -1
- package/dist/commands/workers.js +57 -15
- package/dist/lib/cli-flag-keys.d.ts +1 -0
- package/dist/lib/cli-flag-keys.js +1 -0
- package/dist/lib/core-utils/session-id.d.ts +6 -0
- package/dist/lib/core-utils/session-id.js +35 -0
- package/dist/lib/daemon/catalog.js +1 -1
- package/dist/lib/daemon/lanes-reconcile-guard.d.ts +6 -5
- package/dist/lib/daemon/lanes-reconcile-guard.js +26 -7
- package/dist/lib/daemon/run.d.ts +6 -0
- package/dist/lib/daemon/run.js +36 -9
- package/dist/lib/daemon/supervisor.d.ts +7 -1
- package/dist/lib/daemon/supervisor.js +50 -4
- package/dist/utils/resident-package-preload.js +4 -0
- package/package.json +1 -1
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
5
6
|
import { spawn, spawnSync } from "node:child_process";
|
|
6
7
|
import { awkCannotOpenFileDiagnostic } from "../lib/core-utils/awk-diagnostics.js";
|
|
7
8
|
import { shellDisplayPath } from "../lib/core-utils/shell-display-path.js";
|
|
9
|
+
import { validSessionId, resolveSessionId as resolveSharedSessionId } from "../lib/core-utils/session-id.js";
|
|
8
10
|
export const HQ_SESSION_ASSET = "core/scripts/hq-session.sh";
|
|
11
|
+
const requireSessionDependency = createRequire(import.meta.url);
|
|
9
12
|
export const HQ_SESSION_HELP = [
|
|
10
13
|
"hq-core: public",
|
|
11
14
|
"hq-session.sh — read/write the current session's metadata.",
|
|
@@ -15,6 +18,9 @@ export const HQ_SESSION_HELP = [
|
|
|
15
18
|
" core/scripts/hq-session.sh path # print path to current meta.yaml",
|
|
16
19
|
" core/scripts/hq-session.sh get <key> # read a key from meta.yaml",
|
|
17
20
|
" core/scripts/hq-session.sh set <key> <value> # set/replace a top-level key",
|
|
21
|
+
" add company <slug> # add a company to the session lock set",
|
|
22
|
+
" remove company <slug> # remove a company from the session lock set",
|
|
23
|
+
" get company_slugs # print locked companies, primary first",
|
|
18
24
|
"",
|
|
19
25
|
"Reserved value:",
|
|
20
26
|
" set company_slug personal # bind the session to no-company (personal) work",
|
|
@@ -105,44 +111,69 @@ const defaultDependencies = () => ({
|
|
|
105
111
|
...(result.error ? { error: result.error } : {}),
|
|
106
112
|
};
|
|
107
113
|
},
|
|
114
|
+
multiCompanyLockEnabled: false,
|
|
108
115
|
});
|
|
109
116
|
function isExpectedFsMiss(error) {
|
|
110
117
|
const code = error?.code;
|
|
111
118
|
return code === "ENOENT" || code === "ENOTDIR" || code === "EACCES" || code === "ELOOP";
|
|
112
119
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
function validSessionId(value) {
|
|
117
|
-
return value.length > 0 && value !== "." && value !== ".." && /^[A-Za-z0-9._-]+$/.test(value);
|
|
118
|
-
}
|
|
119
|
-
function sessionIdFromEnv(env) {
|
|
120
|
-
for (const name of [
|
|
121
|
-
"HQ_SESSION_ID", "CLAUDE_CODE_SESSION_ID", "CLAUDE_SESSION_ID",
|
|
122
|
-
"CODEX_SESSION_ID", "CODEX_THREAD_ID", "GROK_SESSION_ID",
|
|
123
|
-
]) {
|
|
124
|
-
const value = asciiWhitespace(env[name] ?? "");
|
|
125
|
-
if (validSessionId(value))
|
|
126
|
-
return value;
|
|
127
|
-
}
|
|
128
|
-
return "";
|
|
120
|
+
export { validSessionId } from "../lib/core-utils/session-id.js";
|
|
121
|
+
function resolveSessionId(root, deps) {
|
|
122
|
+
return resolveSharedSessionId(root, deps.env, deps.readFile);
|
|
129
123
|
}
|
|
130
|
-
function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return "";
|
|
124
|
+
function isRegisteredCompany(root, slug, deps) {
|
|
125
|
+
if (!slug || slug.startsWith("_") || slug === "companies" || slug === "unaffiliated_repos")
|
|
126
|
+
return false;
|
|
134
127
|
try {
|
|
135
|
-
const
|
|
136
|
-
|
|
128
|
+
const yaml = requireSessionDependency("js-yaml");
|
|
129
|
+
const value = yaml.load(deps.readFile(path.join(root, "companies", "manifest.yaml"), "utf8"));
|
|
130
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
131
|
+
return false;
|
|
132
|
+
const companies = value.companies;
|
|
133
|
+
return Boolean(companies && typeof companies === "object" && !Array.isArray(companies) && Object.hasOwn(companies, slug));
|
|
137
134
|
}
|
|
138
|
-
catch
|
|
139
|
-
|
|
140
|
-
return "";
|
|
141
|
-
throw error;
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
142
137
|
}
|
|
143
138
|
}
|
|
144
|
-
function
|
|
145
|
-
|
|
139
|
+
function withCompanyLock(root, sessionId, action) {
|
|
140
|
+
const lock = path.join(root, "workspace", "sessions", sessionId, ".company-lock-set.lock");
|
|
141
|
+
const deadline = Date.now() + 30_000;
|
|
142
|
+
while (true) {
|
|
143
|
+
try {
|
|
144
|
+
fs.mkdirSync(lock);
|
|
145
|
+
fs.writeFileSync(path.join(lock, "pid"), `${process.pid}\n`, "utf8");
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error.code !== "EEXIST")
|
|
150
|
+
throw error;
|
|
151
|
+
try {
|
|
152
|
+
const owner = Number.parseInt(fs.readFileSync(path.join(lock, "pid"), "utf8").trim(), 10);
|
|
153
|
+
if (Number.isInteger(owner) && owner > 0) {
|
|
154
|
+
try {
|
|
155
|
+
process.kill(owner, 0);
|
|
156
|
+
}
|
|
157
|
+
catch (probeError) {
|
|
158
|
+
if (probeError.code === "ESRCH") {
|
|
159
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch { /* The shell writer may be between mkdir and writing pid. */ }
|
|
166
|
+
if (Date.now() >= deadline)
|
|
167
|
+
throw new Error("hq-session: timed out waiting for company lock update");
|
|
168
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
return action();
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
fs.rmSync(lock, { recursive: true, force: true });
|
|
176
|
+
}
|
|
146
177
|
}
|
|
147
178
|
function findMetaValue(contents, key) {
|
|
148
179
|
for (const line of contents.split("\n")) {
|
|
@@ -155,6 +186,50 @@ function findMetaValue(contents, key) {
|
|
|
155
186
|
return { found: false, value: "" };
|
|
156
187
|
}
|
|
157
188
|
function getMetaValue(contents, key) { return findMetaValue(contents, key).value; }
|
|
189
|
+
function writeCompanyMetadata(contents, companies) {
|
|
190
|
+
const primary = companies[0] ?? "";
|
|
191
|
+
const records = contents.split("\n");
|
|
192
|
+
if (records.at(-1) === "")
|
|
193
|
+
records.pop();
|
|
194
|
+
const replaced = [];
|
|
195
|
+
let havePrimary = false;
|
|
196
|
+
let haveCompanies = false;
|
|
197
|
+
for (const line of records) {
|
|
198
|
+
const first = line.match(/^\s*(\S+)/)?.[1] ?? "";
|
|
199
|
+
if (first === "company_slug:") {
|
|
200
|
+
if (!havePrimary)
|
|
201
|
+
replaced.push(`company_slug: ${primary}`);
|
|
202
|
+
havePrimary = true;
|
|
203
|
+
}
|
|
204
|
+
else if (first === "company_slugs:") {
|
|
205
|
+
if (!haveCompanies)
|
|
206
|
+
replaced.push(`company_slugs: ${companies.join(",")}`);
|
|
207
|
+
haveCompanies = true;
|
|
208
|
+
}
|
|
209
|
+
else
|
|
210
|
+
replaced.push(line);
|
|
211
|
+
}
|
|
212
|
+
if (!havePrimary)
|
|
213
|
+
replaced.push(`company_slug: ${primary}`);
|
|
214
|
+
if (!haveCompanies)
|
|
215
|
+
replaced.push(`company_slugs: ${companies.join(",")}`);
|
|
216
|
+
return `${replaced.join("\n")}\n`;
|
|
217
|
+
}
|
|
218
|
+
function metaCompanySlugs(contents) {
|
|
219
|
+
const listed = getMetaValue(contents, "company_slugs");
|
|
220
|
+
const primary = getMetaValue(contents, "company_slug");
|
|
221
|
+
const candidates = listed ? listed.split(",") : primary ? [primary] : [];
|
|
222
|
+
const seen = new Set();
|
|
223
|
+
const result = candidates.filter((slug) => {
|
|
224
|
+
if (!/^[A-Za-z0-9_-]+$/.test(slug) || seen.has(slug))
|
|
225
|
+
return false;
|
|
226
|
+
seen.add(slug);
|
|
227
|
+
return true;
|
|
228
|
+
});
|
|
229
|
+
if (result.length > 1 && result.includes("personal"))
|
|
230
|
+
return primary ? [primary] : [];
|
|
231
|
+
return primary && result[0] !== primary ? [primary] : result;
|
|
232
|
+
}
|
|
158
233
|
const WORK_MESH_SESSION_START_COMMAND = 'event=$HQ_CORE_SESSION_EVENT_JSON; unset HQ_CORE_SESSION_EVENT_JSON; printf "%s" "$event" | bash "$1" SessionStart';
|
|
159
234
|
function recordWorkMeshSessionStart(input) {
|
|
160
235
|
const hook = path.join(input.root, "core", "hooks", "SessionStart", "35-work-mesh-session-start.sh");
|
|
@@ -218,8 +293,33 @@ function runWorkMeshProjectRebind(input) {
|
|
|
218
293
|
}
|
|
219
294
|
return { stdout: result.stdout, stderr: result.stderr };
|
|
220
295
|
}
|
|
296
|
+
function registerWorkMeshCompany(root, sessionId, company, metadata, cwd, deps) {
|
|
297
|
+
const library = path.join(root, "core", "scripts", "lib");
|
|
298
|
+
if (!deps.isFile(path.join(library, "work-mesh-enqueue.sh")) ||
|
|
299
|
+
!deps.isFile(path.join(library, "work-mesh-live-rebind.sh")))
|
|
300
|
+
return { stdout: "", stderr: "" };
|
|
301
|
+
const script = [
|
|
302
|
+
'. "$1/work-mesh-enqueue.sh" 2>/dev/null || true',
|
|
303
|
+
'. "$1/work-mesh-live-hook.sh" 2>/dev/null || true',
|
|
304
|
+
'. "$1/work-mesh-live-rebind.sh" 2>/dev/null || true',
|
|
305
|
+
'if command -v work_mesh_live_write_binding_marker >/dev/null 2>&1; then work_mesh_live_write_binding_marker "$2" "$3" "$4" "$5" || true; fi',
|
|
306
|
+
].join("\n");
|
|
307
|
+
const result = deps.runCommand("bash", ["-c", script, "_", library, sessionId, company,
|
|
308
|
+
getMetaValue(metadata, "project"), getMetaValue(metadata, "task")], cwd, {
|
|
309
|
+
...deps.env,
|
|
310
|
+
HQ_ROOT: root,
|
|
311
|
+
PWD: deps.env.PWD || cwd,
|
|
312
|
+
});
|
|
313
|
+
if (result.error) {
|
|
314
|
+
const code = result.error.code;
|
|
315
|
+
if (code === "ENOENT" || code === "ENOTDIR")
|
|
316
|
+
return { stdout: "", stderr: "" };
|
|
317
|
+
throw result.error;
|
|
318
|
+
}
|
|
319
|
+
return { stdout: result.stdout, stderr: result.stderr };
|
|
320
|
+
}
|
|
221
321
|
function recordHqSessionWorkMeshEffects(input, deps) {
|
|
222
|
-
if (input.key === "company_slug" && input.value && input.value !== input.previous && input.value !== "personal") {
|
|
322
|
+
if ((input.key === "company_slug" || input.key === "company_slugs") && input.value && input.value !== input.previous && input.value !== "personal") {
|
|
223
323
|
const stderr = recordWorkMeshSessionStart({
|
|
224
324
|
sessionId: input.sessionId,
|
|
225
325
|
root: input.root,
|
|
@@ -443,14 +543,18 @@ function atomicWrite(filePath, contents, deps) {
|
|
|
443
543
|
throw error;
|
|
444
544
|
}
|
|
445
545
|
}
|
|
446
|
-
function mintScopeCapability(root, sessionId,
|
|
447
|
-
|
|
546
|
+
function mintScopeCapability(root, sessionId, companies, deps, includeSet = false) {
|
|
547
|
+
const company = companies[0] ?? "";
|
|
548
|
+
if (!root || !sessionId || !company || companies.some((slug) => !/^[A-Za-z0-9_-]+$/.test(slug)))
|
|
448
549
|
return false;
|
|
449
550
|
const directory = path.join(root, "workspace", "sessions", sessionId);
|
|
450
551
|
const filePath = path.join(directory, "scope-capability.json");
|
|
451
552
|
try {
|
|
452
553
|
deps.mkdir(directory);
|
|
453
|
-
const
|
|
554
|
+
const mintedAt = timestamp(deps.now());
|
|
555
|
+
const value = includeSet
|
|
556
|
+
? { session_id: sessionId, company_slug: company, minted_at: mintedAt, company_slugs: companies }
|
|
557
|
+
: { session_id: sessionId, company_slug: company, minted_at: mintedAt };
|
|
454
558
|
atomicWrite(filePath, `${JSON.stringify(value, null, 2)}\n`, deps);
|
|
455
559
|
return true;
|
|
456
560
|
}
|
|
@@ -489,7 +593,7 @@ export function executeHqSession(argv, context, overrides = {}) {
|
|
|
489
593
|
if (!subcommand || subcommand === "-h" || subcommand === "--help" || subcommand === "help") {
|
|
490
594
|
return { code: 0, stdout: HQ_SESSION_HELP, stderr: "" };
|
|
491
595
|
}
|
|
492
|
-
if (!["current", "path", "get", "set"].includes(subcommand)) {
|
|
596
|
+
if (!["current", "path", "get", "set", "add", "remove"].includes(subcommand)) {
|
|
493
597
|
return { code: 1, stdout: "", stderr: `hq-session: unknown subcommand '${subcommand}'\n${HQ_SESSION_HELP}` };
|
|
494
598
|
}
|
|
495
599
|
if (subcommand === "current") {
|
|
@@ -505,7 +609,15 @@ export function executeHqSession(argv, context, overrides = {}) {
|
|
|
505
609
|
if (!metaPath || !deps.isFile(metaPath))
|
|
506
610
|
return { code: 0, stdout: "", stderr: "" };
|
|
507
611
|
try {
|
|
508
|
-
const
|
|
612
|
+
const contents = deps.readFile(metaPath, "utf8");
|
|
613
|
+
if (key === "company_slugs") {
|
|
614
|
+
const primary = getMetaValue(contents, "company_slug");
|
|
615
|
+
const slugs = deps.multiCompanyLockEnabled
|
|
616
|
+
? metaCompanySlugs(contents).filter((slug) => slug === "personal" || exactCompanyDir(root, slug, deps))
|
|
617
|
+
: primary ? [primary] : [];
|
|
618
|
+
return { code: 0, stdout: slugs.length ? `${slugs.join(",")}\n` : "", stderr: "" };
|
|
619
|
+
}
|
|
620
|
+
const result = findMetaValue(contents, key);
|
|
509
621
|
return { code: 0, stdout: result.found ? `${result.value}\n` : "", stderr: "" };
|
|
510
622
|
}
|
|
511
623
|
catch (error) {
|
|
@@ -517,10 +629,71 @@ export function executeHqSession(argv, context, overrides = {}) {
|
|
|
517
629
|
throw error;
|
|
518
630
|
}
|
|
519
631
|
}
|
|
632
|
+
if (subcommand === "add" || subcommand === "remove") {
|
|
633
|
+
const kind = args[0] ?? "";
|
|
634
|
+
const slug = args[1] ?? "";
|
|
635
|
+
if (kind !== "company" || args.length < 2 || !slug) {
|
|
636
|
+
return { code: 1, stdout: "", stderr: `usage: hq-session.sh ${subcommand} company <slug>\n` };
|
|
637
|
+
}
|
|
638
|
+
if (subcommand === "add" && !deps.multiCompanyLockEnabled) {
|
|
639
|
+
return { code: 1, stdout: "", stderr: "hq-session: adding a company is unavailable because hooks.multi-company-session-lock is off\n" };
|
|
640
|
+
}
|
|
641
|
+
if (!/^[A-Za-z0-9_-]+$/.test(slug)) {
|
|
642
|
+
return { code: 1, stdout: "", stderr: `hq-session: invalid company_slug: '${slug}' (allowed characters: A-Z a-z 0-9 - _)\n` };
|
|
643
|
+
}
|
|
644
|
+
const dir = path.join(root, "companies", slug);
|
|
645
|
+
if (subcommand === "add" && (slug === "personal" || !isRegisteredCompany(root, slug, deps) || deps.isSymlink(dir) || hasCompanyCaseAlias(root, slug, deps) || !exactCompanyDir(root, slug, deps))) {
|
|
646
|
+
return { code: 1, stdout: "", stderr: `hq-session: company_slug must use the exact real directory name: '${slug}'\n` };
|
|
647
|
+
}
|
|
648
|
+
const sessionId = sessionOverride || resolveSessionId(root, deps);
|
|
649
|
+
if (!sessionId)
|
|
650
|
+
return { code: 1, stdout: "", stderr: "hq-session: no current session — pass --session-id <id> to target a session explicitly.\n" };
|
|
651
|
+
deps.mkdir(path.join(root, "workspace", "sessions", sessionId));
|
|
652
|
+
return withCompanyLock(root, sessionId, () => {
|
|
653
|
+
const target = path.join(root, "workspace", "sessions", sessionId, "meta.yaml");
|
|
654
|
+
try {
|
|
655
|
+
deps.mkdir(path.dirname(target));
|
|
656
|
+
const existed = deps.isFile(target);
|
|
657
|
+
const oldText = existed ? deps.readFile(target, "utf8") : `session_id: ${sessionId}\nstarted_at: "${timestamp(deps.now())}"\nsenior: user\n`;
|
|
658
|
+
const rawCompanies = metaCompanySlugs(oldText);
|
|
659
|
+
const companies = rawCompanies.filter((company) => company === "personal" || exactCompanyDir(root, company, deps));
|
|
660
|
+
if (subcommand === "add" && companies.includes(slug))
|
|
661
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
662
|
+
const next = subcommand === "add"
|
|
663
|
+
? [...(companies[0] === "personal" ? [] : companies), slug]
|
|
664
|
+
: companies.filter((value) => value !== slug);
|
|
665
|
+
if (!next.length)
|
|
666
|
+
return { code: 1, stdout: "", stderr: "hq-session: cannot remove the last locked company; use set company_slug <slug> instead\n" };
|
|
667
|
+
if (subcommand === "remove" && !rawCompanies.includes(slug))
|
|
668
|
+
return { code: 0, stdout: "", stderr: "" };
|
|
669
|
+
const updatedText = writeCompanyMetadata(oldText, next);
|
|
670
|
+
atomicWrite(target, updatedText, deps);
|
|
671
|
+
if (!mintScopeCapability(root, sessionId, next, deps, true)) {
|
|
672
|
+
if (existed)
|
|
673
|
+
atomicWrite(target, oldText, deps);
|
|
674
|
+
else
|
|
675
|
+
deps.remove(target);
|
|
676
|
+
return { code: 1, stdout: "", stderr: `hq-session: failed to mint scope-capability for session ${sessionId}; metadata was restored\n` };
|
|
677
|
+
}
|
|
678
|
+
const stdout = subcommand === "add" ? emitHardPolicies(root, slug, deps) : "";
|
|
679
|
+
const primaryChanged = next[0] !== companies[0];
|
|
680
|
+
const workMeshOutput = primaryChanged
|
|
681
|
+
? registerWorkMeshCompany(root, sessionId, next[0], updatedText, context.cwd, deps)
|
|
682
|
+
: { stdout: "", stderr: "" };
|
|
683
|
+
return { code: 0, stdout: stdout + workMeshOutput.stdout, stderr: workMeshOutput.stderr };
|
|
684
|
+
}
|
|
685
|
+
catch (error) {
|
|
686
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
687
|
+
return { code: 1, stdout: "", stderr: `${message}\n` };
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
}
|
|
520
691
|
const key = args[0] ?? "";
|
|
521
692
|
const value = args[1] ?? "";
|
|
522
693
|
if (!key || args.length < 2)
|
|
523
694
|
return { code: 1, stdout: "", stderr: "usage: hq-session.sh set <key> <value>\n" };
|
|
695
|
+
if (key === "company_slugs")
|
|
696
|
+
return { code: 1, stdout: "", stderr: "hq-session: set company_slugs is unsupported; use add/remove company to update the validated lock set\n" };
|
|
524
697
|
if (key === "company_slug" && value) {
|
|
525
698
|
if (!/^[A-Za-z0-9_-]+$/.test(value)) {
|
|
526
699
|
return { code: 1, stdout: "", stderr: `hq-session: invalid company_slug: '${value}' (allowed characters: A-Z a-z 0-9 - _)\n` };
|
|
@@ -548,35 +721,39 @@ export function executeHqSession(argv, context, overrides = {}) {
|
|
|
548
721
|
const initial = `session_id: ${sessionId}\nstarted_at: "${timestamp(deps.now())}"\nsenior: user\n`;
|
|
549
722
|
deps.writeFile(target, initial, 0o666 & ~deps.umask());
|
|
550
723
|
}
|
|
551
|
-
let previous = "";
|
|
552
|
-
try {
|
|
553
|
-
previous = getMetaValue(deps.readFile(target, "utf8"), key);
|
|
554
|
-
}
|
|
555
|
-
catch {
|
|
556
|
-
previous = "";
|
|
557
|
-
}
|
|
558
724
|
const oldText = deps.readFile(target, "utf8");
|
|
725
|
+
const previous = getMetaValue(oldText, key);
|
|
726
|
+
const companySetChanged = key === "company_slug" && value !== ""
|
|
727
|
+
&& JSON.stringify(metaCompanySlugs(oldText)) !== JSON.stringify([value]);
|
|
559
728
|
const records = oldText.split("\n");
|
|
560
729
|
if (records.at(-1) === "")
|
|
561
730
|
records.pop();
|
|
731
|
+
const clearProjectTask = key === "company_slug" && previous !== "" && value !== previous;
|
|
562
732
|
let found = false;
|
|
563
|
-
const replaced =
|
|
733
|
+
const replaced = [];
|
|
734
|
+
for (const line of records) {
|
|
564
735
|
const first = line.match(/^\s*(\S+)/)?.[1] ?? "";
|
|
565
|
-
if (first
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
736
|
+
if (clearProjectTask && (first === "project:" || first === "task:"))
|
|
737
|
+
continue;
|
|
738
|
+
if (first === `${key}:`) {
|
|
739
|
+
found = true;
|
|
740
|
+
replaced.push(`${key}: ${value}`);
|
|
741
|
+
}
|
|
742
|
+
else
|
|
743
|
+
replaced.push(line);
|
|
744
|
+
}
|
|
570
745
|
if (!found)
|
|
571
746
|
replaced.push(`${key}: ${value}`);
|
|
572
|
-
|
|
747
|
+
let updatedText = `${replaced.join("\n")}\n`;
|
|
748
|
+
if (key === "company_slug")
|
|
749
|
+
updatedText = writeCompanyMetadata(updatedText, value ? [value] : []);
|
|
573
750
|
atomicWrite(target, updatedText, deps);
|
|
574
751
|
let stdout = "";
|
|
575
|
-
if (key === "company_slug" && value &&
|
|
576
|
-
if (!mintScopeCapability(root, sessionId, value, deps)) {
|
|
752
|
+
if (key === "company_slug" && value && companySetChanged) {
|
|
753
|
+
if (!mintScopeCapability(root, sessionId, [value], deps, true)) {
|
|
577
754
|
return { code: 1, stdout: "", stderr: `hq-session: failed to mint scope-capability for session ${sessionId}\n` };
|
|
578
755
|
}
|
|
579
|
-
if (value !== "personal")
|
|
756
|
+
if (value !== previous && value !== "personal")
|
|
580
757
|
stdout = emitHardPolicies(root, value, deps);
|
|
581
758
|
}
|
|
582
759
|
const workMeshOutput = recordHqSessionWorkMeshEffects({
|
|
@@ -597,7 +774,20 @@ export function executeHqSession(argv, context, overrides = {}) {
|
|
|
597
774
|
}
|
|
598
775
|
/** Signature consumed by the lightweight fast-core dispatcher. */
|
|
599
776
|
export async function runHqSessionFast(argv, _stdin, cwd, hqRoot) {
|
|
600
|
-
const
|
|
777
|
+
const commandArgs = argv[0] === "--session-id"
|
|
778
|
+
? argv.slice(2)
|
|
779
|
+
: argv[0]?.startsWith("--session-id=") ? argv.slice(1) : argv;
|
|
780
|
+
const needsMultiCompanyFlag = commandArgs[0] === "add"
|
|
781
|
+
|| (commandArgs[0] === "get" && commandArgs[1] === "company_slugs");
|
|
782
|
+
let enabled = false;
|
|
783
|
+
if (needsMultiCompanyFlag) {
|
|
784
|
+
const [{ fastFlagEnabled }, { MULTI_COMPANY_SESSION_LOCK_FLAG }] = await Promise.all([
|
|
785
|
+
import("../native-fast-route.js"),
|
|
786
|
+
import("../lib/cli-flag-keys.js"),
|
|
787
|
+
]);
|
|
788
|
+
enabled = await fastFlagEnabled(MULTI_COMPANY_SESSION_LOCK_FLAG);
|
|
789
|
+
}
|
|
790
|
+
const result = executeHqSession(argv, { cwd, hqRoot }, { multiCompanyLockEnabled: enabled });
|
|
601
791
|
if (result.stdout)
|
|
602
792
|
process.stdout.write(result.stdout);
|
|
603
793
|
if (result.stderr)
|
|
@@ -23,6 +23,7 @@ export type ResolveCompanyDependencies = {
|
|
|
23
23
|
onHqExit?: (args: readonly string[]) => void;
|
|
24
24
|
pid?: number;
|
|
25
25
|
random?: () => number;
|
|
26
|
+
multiCompanyLockEnabled?: boolean;
|
|
26
27
|
};
|
|
27
28
|
export declare function resolveCompany(args: readonly string[], dependencies: ResolveCompanyDependencies): Promise<ResolveCompanyResult>;
|
|
28
29
|
export declare function runResolveCompanyFast(args: readonly string[], stdin: string, cwd: string, hqRoot: string | undefined, _commandName?: string, _hqRootArg?: string, stdinReader?: () => Promise<string>): Promise<number>;
|
|
@@ -4,6 +4,7 @@ import * as fs from "node:fs";
|
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { awkCannotOpenFileDiagnostic } from "../lib/core-utils/awk-diagnostics.js";
|
|
6
6
|
import { buildCmdExeSpawnPlan, isWindowsBatchFile } from "../utils/windows-spawn.js";
|
|
7
|
+
import { fastFlagEnabled } from "../native-fast-route.js";
|
|
7
8
|
export const RESOLVE_COMPANY_ASSET = "core/scripts/resolve-company.sh";
|
|
8
9
|
export const RESOLVE_COMPANY_HELP = [
|
|
9
10
|
"hq-core: public",
|
|
@@ -143,7 +144,7 @@ function sessionId(root, env, fileSystem) {
|
|
|
143
144
|
const current = readFile(path.join(root, "workspace/sessions/.current"), fileSystem)?.replace(/[ \t\n\v\f\r]/g, "") ?? "";
|
|
144
145
|
return validSessionId(current) ? current : "";
|
|
145
146
|
}
|
|
146
|
-
function sessionCompany(root, env, fileSystem) {
|
|
147
|
+
function sessionCompany(root, env, fileSystem, multiCompanyLockEnabled = false) {
|
|
147
148
|
const helper = path.join(root, "core/scripts/hq-session.sh");
|
|
148
149
|
try {
|
|
149
150
|
fileSystem.accessSync(helper, fs.constants.X_OK);
|
|
@@ -151,19 +152,19 @@ function sessionCompany(root, env, fileSystem) {
|
|
|
151
152
|
catch (error) {
|
|
152
153
|
const code = errorCode(error);
|
|
153
154
|
if (code === "ENOENT" || code === "ENOTDIR" || code === "EACCES" || code === "ELOOP")
|
|
154
|
-
return "";
|
|
155
|
+
return { company: "", companies: [] };
|
|
155
156
|
throw error;
|
|
156
157
|
}
|
|
157
158
|
// A directory may be executable according to access(2), but `bash directory`
|
|
158
159
|
// fails without output, just as hq-session.sh does here.
|
|
159
160
|
if (!isFile(helper, fileSystem))
|
|
160
|
-
return "";
|
|
161
|
+
return { company: "", companies: [] };
|
|
161
162
|
const id = sessionId(root, env, fileSystem);
|
|
162
163
|
if (!id)
|
|
163
|
-
return "";
|
|
164
|
+
return { company: "", companies: [] };
|
|
164
165
|
const meta = readFile(path.join(root, "workspace/sessions", id, "meta.yaml"), fileSystem);
|
|
165
166
|
if (meta === undefined)
|
|
166
|
-
return "";
|
|
167
|
+
return { company: "", companies: [] };
|
|
167
168
|
let value = "";
|
|
168
169
|
for (const line of meta.split("\n")) {
|
|
169
170
|
const firstField = line.replace(/^[ \t]+/, "").split(/[ \t]+/, 1)[0];
|
|
@@ -174,7 +175,30 @@ function sessionCompany(root, env, fileSystem) {
|
|
|
174
175
|
}
|
|
175
176
|
// The shell's command substitution strips trailing newlines, then tr removes
|
|
176
177
|
// all remaining whitespace and double quotes from the helper's output.
|
|
177
|
-
|
|
178
|
+
value = value.replace(/[ \t\n\v\f\r"]/g, "");
|
|
179
|
+
if (!value || !multiCompanyLockEnabled)
|
|
180
|
+
return { company: value, companies: value ? [value] : [] };
|
|
181
|
+
let listed = "";
|
|
182
|
+
for (const line of meta.split("\n")) {
|
|
183
|
+
if (line.replace(/^[ \t]+/, "").split(/[ \t]+/, 1)[0] !== "company_slugs:")
|
|
184
|
+
continue;
|
|
185
|
+
listed = line.replace(/^[^:]+:[ \t]*/, "").replace(/^"|"$/g, "");
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
const companies = [...new Set((listed ? listed.split(",") : [value]).filter((slug) => /^[A-Za-z0-9_-]+$/.test(slug) &&
|
|
189
|
+
(slug === "personal" || (() => {
|
|
190
|
+
try {
|
|
191
|
+
return fileSystem.statSync(path.join(root, "companies", slug)).isDirectory() && !fileSystem.lstatSync(path.join(root, "companies", slug)).isSymbolicLink();
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
})())))];
|
|
197
|
+
if (companies[0] !== value)
|
|
198
|
+
return { company: "", companies: [] };
|
|
199
|
+
if (companies.length > 1 && companies.includes("personal"))
|
|
200
|
+
return { company: value, companies: [value] };
|
|
201
|
+
return { company: value, companies };
|
|
178
202
|
}
|
|
179
203
|
function fleetIdentityPresent(rootPrefix, env, fileSystem) {
|
|
180
204
|
if (env.HQ_AGENT_WORKDIR || env.HQ_AGENT_COMPANY_DIR || env.HQ_AGENT_IDENTITY_FILE)
|
|
@@ -386,8 +410,9 @@ function runHq(args, env, timeoutMs, cwd, command = "hq", onExitObserved) {
|
|
|
386
410
|
});
|
|
387
411
|
});
|
|
388
412
|
}
|
|
389
|
-
function output(company, source) {
|
|
390
|
-
|
|
413
|
+
function output(company, source, companies) {
|
|
414
|
+
const payload = { company, source, ...(source === "session" && companies ? { companies } : {}) };
|
|
415
|
+
return { code: 0, stdout: Buffer.from(`${JSON.stringify(payload)}\n`), stderr: Buffer.alloc(0) };
|
|
391
416
|
}
|
|
392
417
|
function parseArgs(args) {
|
|
393
418
|
let root = "";
|
|
@@ -519,9 +544,13 @@ export async function resolveCompany(args, dependencies) {
|
|
|
519
544
|
const promptMatch = promptSlug(prompt, slugs);
|
|
520
545
|
if (promptMatch)
|
|
521
546
|
return output(promptMatch, "prompt");
|
|
522
|
-
const sessionMatch = sessionCompany(root, env, fileSystem);
|
|
523
|
-
if (sessionMatch && knownSlugs.has(sessionMatch))
|
|
524
|
-
|
|
547
|
+
const sessionMatch = sessionCompany(root, env, fileSystem, dependencies.multiCompanyLockEnabled);
|
|
548
|
+
if (sessionMatch.company && knownSlugs.has(sessionMatch.company)) {
|
|
549
|
+
const companies = sessionMatch.companies.filter((slug) => knownSlugs.has(slug));
|
|
550
|
+
return dependencies.multiCompanyLockEnabled
|
|
551
|
+
? output(sessionMatch.company, "session", companies.length ? companies : [sessionMatch.company])
|
|
552
|
+
: output(sessionMatch.company, "session");
|
|
553
|
+
}
|
|
525
554
|
if (fleetIdentityPresent(env.HQ_AGENT_ROOT_PREFIX ?? "", env, fileSystem))
|
|
526
555
|
return output("", "none");
|
|
527
556
|
// jq is intentionally no longer needed. The native route is always hosted by
|
|
@@ -574,6 +603,7 @@ export async function runResolveCompanyFast(args, stdin, cwd, hqRoot, _commandNa
|
|
|
574
603
|
hqRoot,
|
|
575
604
|
stdin: actualStdin,
|
|
576
605
|
stdinIsTty: process.stdin.isTTY === true,
|
|
606
|
+
multiCompanyLockEnabled: await fastFlagEnabled("hooks.multi-company-session-lock"),
|
|
577
607
|
});
|
|
578
608
|
process.stdout.write(result.stdout);
|
|
579
609
|
if (result.stderr.length)
|
|
@@ -2,6 +2,11 @@ import type { Command } from "commander";
|
|
|
2
2
|
export { buildSecretUsage, emitEnvExportsAndExit, loadRevealedSecrets, parseSecretNameList, runSecretsEnvCommand, } from "../secrets-env-action.js";
|
|
3
3
|
export type { SecretLoadResponse, SecretScriptLockMode, SecretTier, SecretUsage, SecretUsageChannel } from "../secrets-env-action.js";
|
|
4
4
|
export type { SecretScriptUsage, SecretMetadata, SecretLoadSuccessRow } from "../secrets-env-action.js";
|
|
5
|
+
type SessionCompanyLock = {
|
|
6
|
+
sessionId: string;
|
|
7
|
+
companies: string[];
|
|
8
|
+
} | undefined;
|
|
9
|
+
export declare function readSessionCompanyLock(env?: NodeJS.ProcessEnv): SessionCompanyLock;
|
|
5
10
|
import { vaultApiFetch, getCompanyUid, getEntityUid } from "../utils/vault-api.js";
|
|
6
11
|
export type { VaultApiOptions } from "../utils/vault-api.js";
|
|
7
12
|
export { vaultApiFetch, getCompanyUid, getEntityUid };
|