@indigoai-us/hq-cli 5.345.156 → 5.345.159
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 +231 -0
- package/assets/scaffold/core/scripts/rebuild-company-knowledge-index.sh +110 -2
- package/assets/scaffold/core/scripts/rebuild-projects-index.sh +101 -5
- package/assets/scaffold/core/scripts/worktree.sh +4 -0
- package/dist/command-catalog.generated.d.ts +531 -6
- package/dist/command-catalog.generated.js +674 -27
- package/dist/command-registration-plan.d.ts +18 -0
- package/dist/command-registration-plan.js +4 -0
- package/dist/commands/_patterns.d.ts +17 -0
- package/dist/commands/_patterns.js +22 -0
- package/dist/commands/agent-kit.js +8 -6
- package/dist/commands/bot.js +7 -4
- package/dist/commands/channel-suggest.d.ts +56 -0
- package/dist/commands/channel-suggest.js +170 -0
- package/dist/commands/channels.js +7 -1
- package/dist/commands/cloud.d.ts +45 -0
- package/dist/commands/cloud.js +150 -10
- package/dist/commands/company.js +4 -3
- package/dist/commands/core-checkpoint-native.d.ts +18 -0
- package/dist/commands/core-checkpoint-native.js +67 -2
- package/dist/commands/core-pipeline-driver-native.d.ts +12 -1
- package/dist/commands/core-pipeline-driver-native.js +131 -5
- package/dist/commands/core.js +12 -0
- package/dist/commands/crm.js +83 -6
- package/dist/commands/deploy-access.d.ts +33 -0
- package/dist/commands/deploy-access.js +322 -0
- package/dist/commands/dm-attach.d.ts +106 -0
- package/dist/commands/dm-attach.js +184 -0
- package/dist/commands/dm.d.ts +37 -0
- package/dist/commands/dm.js +172 -10
- package/dist/commands/feedback-show.d.ts +40 -0
- package/dist/commands/feedback-show.js +261 -0
- package/dist/commands/feedback.d.ts +21 -0
- package/dist/commands/feedback.js +152 -2
- package/dist/commands/files.d.ts +24 -0
- package/dist/commands/files.js +52 -1
- package/dist/commands/integrations-connect.js +7 -3
- package/dist/commands/lanes-accounts.js +16 -1
- package/dist/commands/lanes-loop.js +14 -4
- package/dist/commands/lanes-message.js +6 -4
- package/dist/commands/lanes-resume.js +36 -13
- package/dist/commands/lanes-shared.d.ts +5 -0
- package/dist/commands/lanes-shared.js +13 -3
- package/dist/commands/lanes-watch.js +1 -0
- package/dist/commands/lanes.d.ts +20 -1
- package/dist/commands/lanes.js +201 -51
- package/dist/commands/mesh.js +149 -1
- package/dist/commands/monitor.d.ts +1 -0
- package/dist/commands/monitor.js +4 -1
- package/dist/commands/repo.d.ts +41 -0
- package/dist/commands/repo.js +331 -0
- package/dist/commands/scan.d.ts +3 -1
- package/dist/commands/scan.js +7 -3
- package/dist/commands/secrets.d.ts +21 -0
- package/dist/commands/secrets.js +114 -14
- package/dist/commands/signals.js +17 -5
- package/dist/commands/staff/listings.d.ts +41 -0
- package/dist/commands/staff/listings.js +160 -0
- package/dist/commands/staff/reset-test-account.d.ts +3 -2
- package/dist/commands/staff/reset-test-account.js +4 -3
- package/dist/hq-cloud-sync-runner/bin/sync-runner.js +2629 -1036
- package/dist/hq-cloud-sync-runner/package.json +1 -1
- package/dist/lib/agent-kit/run/inbox.js +2 -1
- package/dist/lib/agent-kit/wake-secrets.d.ts +7 -0
- package/dist/lib/agent-kit/wake-secrets.js +32 -0
- package/dist/lib/agent-kit/wake.d.ts +4 -0
- package/dist/lib/agent-kit/wake.js +49 -3
- package/dist/lib/bot/inflight.d.ts +7 -1
- package/dist/lib/bot/inflight.js +10 -2
- package/dist/lib/bot/prompt.js +3 -0
- package/dist/lib/bot/run.js +72 -20
- package/dist/lib/bot/session.d.ts +14 -0
- package/dist/lib/bot/session.js +70 -0
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +6 -0
- package/dist/lib/capacity/collectors/claude-oauth.js +12 -0
- package/dist/lib/capacity/lane-login.d.ts +6 -0
- package/dist/lib/capacity/lane-login.js +5 -0
- package/dist/lib/core-utils/worktree.js +2 -0
- package/dist/lib/daemon/exit-cause.d.ts +43 -0
- package/dist/lib/daemon/exit-cause.js +88 -0
- package/dist/lib/daemon/handoff.d.ts +2 -0
- package/dist/lib/daemon/handoff.js +6 -1
- package/dist/lib/daemon/hq-child-bootstrap.mjs +20 -3
- package/dist/lib/daemon/paths.d.ts +2 -0
- package/dist/lib/daemon/paths.js +1 -0
- package/dist/lib/daemon/run.js +13 -0
- package/dist/lib/daemon/supervisor.d.ts +54 -1
- package/dist/lib/daemon/supervisor.js +297 -21
- package/dist/lib/daemon/unit-holds.d.ts +22 -0
- package/dist/lib/daemon/unit-holds.js +86 -0
- package/dist/lib/flag-registry-worker-source.generated.d.ts +1 -1
- package/dist/lib/flag-registry-worker-source.generated.js +1 -1
- package/dist/lib/index-render/company-knowledge.js +1 -3
- package/dist/lib/index-render/projects.js +1 -3
- package/dist/lib/lanes/account-company.d.ts +28 -0
- package/dist/lib/lanes/account-company.js +53 -0
- package/dist/lib/lanes/accounts.d.ts +22 -3
- package/dist/lib/lanes/accounts.js +38 -2
- package/dist/lib/lanes/claude-balance.d.ts +6 -1
- package/dist/lib/lanes/claude-balance.js +6 -1
- package/dist/lib/lanes/claude-credential.d.ts +78 -0
- package/dist/lib/lanes/claude-credential.js +163 -0
- package/dist/lib/lanes/compaction.d.ts +2 -0
- package/dist/lib/lanes/compaction.js +9 -0
- package/dist/lib/lanes/config.js +6 -0
- package/dist/lib/lanes/envelope-repair-dispatch.js +2 -1
- package/dist/lib/lanes/hooks-install.js +2 -1
- package/dist/lib/lanes/host-capacity.d.ts +2 -0
- package/dist/lib/lanes/host-capacity.js +1 -0
- package/dist/lib/lanes/keep-alive.d.ts +1 -1
- package/dist/lib/lanes/keep-alive.js +3 -1
- package/dist/lib/lanes/links.js +10 -4
- package/dist/lib/lanes/loop.js +16 -1
- package/dist/lib/lanes/node-binary.d.ts +17 -0
- package/dist/lib/lanes/node-binary.js +30 -0
- package/dist/lib/lanes/outpost-dispatch.d.ts +2 -1
- package/dist/lib/lanes/outpost-dispatch.js +1 -0
- package/dist/lib/lanes/reconcile.js +4 -0
- package/dist/lib/lanes/remote-control-lane.js +2 -0
- package/dist/lib/lanes/spawn.d.ts +3 -0
- package/dist/lib/lanes/spawn.js +119 -9
- package/dist/lib/lanes/store.d.ts +6 -0
- package/dist/lib/lanes/store.js +107 -18
- package/dist/lib/lanes/types.d.ts +2 -0
- package/dist/lib/lanes/watch.js +28 -5
- package/dist/lib/lanes/watcher-main.js +4 -3
- package/dist/lib/lanes/watcher.d.ts +4 -0
- package/dist/lib/lanes/watcher.js +110 -21
- package/dist/lib/mesh/api.d.ts +32 -0
- package/dist/lib/mesh/api.js +75 -0
- package/dist/lib/secret-scan/detect.js +9 -0
- package/dist/lib/secret-scan/repo-config.d.ts +41 -0
- package/dist/lib/secret-scan/repo-config.js +237 -0
- package/dist/lib/secret-scan/rules.d.ts +7 -1
- package/dist/lib/secret-scan/rules.js +3 -1
- package/dist/lib/secret-scan/staged.d.ts +5 -0
- package/dist/lib/secret-scan/staged.js +19 -2
- package/dist/lib/secret-scan/types.d.ts +2 -0
- package/dist/lib/sync/server-owned-local-only.d.ts +29 -0
- package/dist/lib/sync/server-owned-local-only.js +75 -0
- package/dist/utils/package-use-lease.js +27 -9
- package/dist/utils/version-gate.d.ts +7 -0
- package/dist/utils/version-gate.js +66 -0
- package/package.json +8 -8
package/dist/commands/mesh.js
CHANGED
|
@@ -13,7 +13,7 @@ import { CLI_VERSION } from "../cli-version.js";
|
|
|
13
13
|
import chalk from "chalk";
|
|
14
14
|
import { ensureCognitoToken, isExpiring, isMachineIdentity, loadCachedTokens, } from "../utils/cognito-session.js";
|
|
15
15
|
import * as readline from "node:readline/promises";
|
|
16
|
-
import { STORY_STATUSES, appendThreadEvent, addProjectMember, ensureProjectThread, eventPayload, getProjectView, listActiveMembershipCompanies, listActiveThreads, meshJson, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, warmMeshConversationCache, } from "../lib/mesh/api.js";
|
|
16
|
+
import { STORY_STATUSES, appendThreadEvent, addProjectMember, ensureProjectThread, eventPayload, getProjectLifecycle, getProjectView, listActiveMembershipCompanies, listActiveThreads, listProjectLifecycles, meshJson, patchStory, putProjectView, registerMeshProject, resolveActiveMembershipCompany, resolveMeshPrincipalUid, setProjectLifecycle, warmMeshConversationCache, } from "../lib/mesh/api.js";
|
|
17
17
|
import { closeSupersededProjectThread, ensureMissingBoardStories, ensureProjectBoard, boardStoryStatus, formatStoryStatusChanges, mergeProjectStories, prdSyncEventsThatChangeBoard, reconcileProjectStories, } from "../lib/mesh/project-board.js";
|
|
18
18
|
import { ACCEPTED_LEDGER_STATES, mapSourceStoryStatus, UnknownProjectStoryStateError } from "../lib/mesh/story-status.js";
|
|
19
19
|
import { buildPreflightFixtures } from "../lib/work-context/preflight-contract.js";
|
|
@@ -665,6 +665,130 @@ async function runProjectMemberAdd(projectId, personUid, opts) {
|
|
|
665
665
|
? `Work mesh: added ${personUid} to project ${projectId}`
|
|
666
666
|
: `Work mesh: ${personUid} is already a member of project ${projectId}`);
|
|
667
667
|
}
|
|
668
|
+
const LIFECYCLE_VERBS = {
|
|
669
|
+
archived: "archive",
|
|
670
|
+
completed: "complete",
|
|
671
|
+
active: "activate",
|
|
672
|
+
};
|
|
673
|
+
/**
|
|
674
|
+
* `hq mesh project archive|complete|activate <projectId>` (feedback 5d596dd0).
|
|
675
|
+
*
|
|
676
|
+
* Moves a Board between active, completed and archived through the server's
|
|
677
|
+
* lifecycle route. Nothing is deleted: stories, the project folder, channel,
|
|
678
|
+
* memberships and history all stay. An archived or completed project leaves
|
|
679
|
+
* the active Board list and the organize candidates, which is how a duplicate
|
|
680
|
+
* Board (one registered under a board id, one under the folder slug) is put
|
|
681
|
+
* away without touching the live one.
|
|
682
|
+
*/
|
|
683
|
+
async function runProjectLifecycle(projectId, status, opts) {
|
|
684
|
+
const verb = LIFECYCLE_VERBS[status];
|
|
685
|
+
const id = projectId.trim();
|
|
686
|
+
if (!id)
|
|
687
|
+
fail(`project ${verb} requires a project id`);
|
|
688
|
+
let expectedVersion;
|
|
689
|
+
if (opts.expectedVersion !== undefined) {
|
|
690
|
+
expectedVersion = Number.parseInt(opts.expectedVersion, 10);
|
|
691
|
+
if (!Number.isSafeInteger(expectedVersion) || expectedVersion < 0) {
|
|
692
|
+
fail("--expected-version must be a non-negative integer");
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
const { token, company } = await withCompany(opts);
|
|
696
|
+
const companyRef = company.companySlug || company.companyUid;
|
|
697
|
+
let current;
|
|
698
|
+
try {
|
|
699
|
+
current = await getProjectLifecycle(token, company.companyUid, id);
|
|
700
|
+
}
|
|
701
|
+
catch (err) {
|
|
702
|
+
if (isRouteUnavailable(err))
|
|
703
|
+
return reportUnavailable("Project lifecycle", `project.${verb}`, opts.json);
|
|
704
|
+
throw err;
|
|
705
|
+
}
|
|
706
|
+
if (current && current.status === status) {
|
|
707
|
+
if (opts.json) {
|
|
708
|
+
console.log(JSON.stringify({ ok: true, action: `project.${verb}`, changed: false, company, ...current }, null, 2));
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
console.log(`Work mesh: project ${id} is already ${status} in company ${companyRef} (nothing changed)`);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
// The server's OCC version is the lifecycle row version, not the Board version.
|
|
715
|
+
const version = expectedVersion ?? current?.version ?? 0;
|
|
716
|
+
let result;
|
|
717
|
+
try {
|
|
718
|
+
result = await setProjectLifecycle(token, company.companyUid, id, {
|
|
719
|
+
status,
|
|
720
|
+
expectedVersion: version,
|
|
721
|
+
...(opts.confirmNonempty ? { confirmNonempty: true } : {}),
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
catch (err) {
|
|
725
|
+
const failure = err;
|
|
726
|
+
const body = (failure.body && typeof failure.body === "object" ? failure.body : {});
|
|
727
|
+
// The list GET above already answered, so the lifecycle routes exist on
|
|
728
|
+
// this server; a 404 here is the project, not the route. Only 405 means
|
|
729
|
+
// the PATCH is missing.
|
|
730
|
+
if (failure.status === 405)
|
|
731
|
+
return reportUnavailable("Project lifecycle", `project.${verb}`, opts.json);
|
|
732
|
+
if (failure.status === 404) {
|
|
733
|
+
fail(`Work mesh project ${id} was not found in company ${companyRef}. Check the id with "hq mesh project lifecycle --company ${companyRef}" or the Board.`);
|
|
734
|
+
}
|
|
735
|
+
if (failure.status === 409 && failure.code === "BOARD_NONEMPTY") {
|
|
736
|
+
const count = Number.isFinite(Number(body.storyCount)) ? Number(body.storyCount) : undefined;
|
|
737
|
+
fail(`Work mesh project ${id} still has ${count !== undefined ? `${count} ` : ""}stor${count === 1 ? "y" : "ies"} on its Board. ` +
|
|
738
|
+
`Re-run with --confirm-nonempty to archive it anyway; the stories, project folder, channel, memberships and history are kept.`);
|
|
739
|
+
}
|
|
740
|
+
if (failure.status === 409) {
|
|
741
|
+
fail(`Work mesh project ${id} lifecycle changed since it was read (expected version ${version}). Re-run the command to retry against the current state.`);
|
|
742
|
+
}
|
|
743
|
+
throw err;
|
|
744
|
+
}
|
|
745
|
+
if (opts.json) {
|
|
746
|
+
console.log(JSON.stringify({ ok: true, action: `project.${verb}`, changed: true, company, previousStatus: current?.status ?? "active", ...result }, null, 2));
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
const detail = status === "active"
|
|
750
|
+
? "it is back on the active Board list and in organize candidates"
|
|
751
|
+
: "it leaves the active Board list and organize candidates; stories, folder, channel, memberships and history are kept";
|
|
752
|
+
console.log(`Work mesh: project ${result.projectId} is now ${result.status} in company ${companyRef} (${detail})`);
|
|
753
|
+
}
|
|
754
|
+
/** `hq mesh project lifecycle [projectId]`: list projects that are not active, or show one. */
|
|
755
|
+
async function runProjectLifecycleList(projectId, opts) {
|
|
756
|
+
const { token, company } = await withCompany(opts);
|
|
757
|
+
const companyRef = company.companySlug || company.companyUid;
|
|
758
|
+
let rows;
|
|
759
|
+
try {
|
|
760
|
+
rows = await listProjectLifecycles(token, company.companyUid);
|
|
761
|
+
}
|
|
762
|
+
catch (err) {
|
|
763
|
+
if (isRouteUnavailable(err))
|
|
764
|
+
return reportUnavailable("Project lifecycle", "project.lifecycle", opts.json);
|
|
765
|
+
throw err;
|
|
766
|
+
}
|
|
767
|
+
const id = projectId?.trim();
|
|
768
|
+
if (id) {
|
|
769
|
+
const row = rows.find((r) => r.projectId === id) ?? null;
|
|
770
|
+
if (opts.json) {
|
|
771
|
+
console.log(JSON.stringify({ ok: true, action: "project.lifecycle", company, projectId: id, lifecycle: row ?? { projectId: id, status: "active", version: 0 } }, null, 2));
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
console.log(row
|
|
775
|
+
? `Work mesh: project ${id} is ${row.status} in company ${companyRef} (lifecycle version ${row.version}, updated ${row.updatedAt || "unknown"})`
|
|
776
|
+
: `Work mesh: project ${id} is active in company ${companyRef} (no lifecycle change recorded)`);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (opts.json) {
|
|
780
|
+
console.log(JSON.stringify({ ok: true, action: "project.lifecycle", company, projects: rows }, null, 2));
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
if (!rows.length) {
|
|
784
|
+
console.log(`Work mesh: no project in company ${companyRef} has been completed or archived`);
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
console.log(`Work mesh: project lifecycle in company ${companyRef}`);
|
|
788
|
+
for (const row of rows.sort((a, b) => a.projectId.localeCompare(b.projectId))) {
|
|
789
|
+
console.log(` ${row.projectId} ${row.status} v${row.version} ${row.updatedAt || ""}`.trimEnd());
|
|
790
|
+
}
|
|
791
|
+
}
|
|
668
792
|
/**
|
|
669
793
|
* Create or merge the project Board view from a parsed PRD, register the
|
|
670
794
|
* project, and verify every PRD story and repo is on the Board. Shared by
|
|
@@ -2690,6 +2814,30 @@ export function registerMeshCommand(program, deps = {}) {
|
|
|
2690
2814
|
.option("--note <text>", "Handoff note")
|
|
2691
2815
|
.option("--json", "Print machine-readable JSON")
|
|
2692
2816
|
.action((projectId, opts) => wrap(() => runProjectReassign(projectId, opts))());
|
|
2817
|
+
for (const status of ["archived", "completed", "active"]) {
|
|
2818
|
+
const verb = LIFECYCLE_VERBS[status];
|
|
2819
|
+
const description = status === "archived"
|
|
2820
|
+
? "Archive a Board (for example a duplicate); keeps stories, folder, channel, members and history"
|
|
2821
|
+
: status === "completed"
|
|
2822
|
+
? "Mark a Board completed; it leaves the active list and organize candidates"
|
|
2823
|
+
: "Restore a completed or archived Board to active";
|
|
2824
|
+
const cmd = project
|
|
2825
|
+
.command(`${verb} <projectId>`)
|
|
2826
|
+
.description(description)
|
|
2827
|
+
.requiredOption("--company <slug|uid>", "Company slug or cloud uid")
|
|
2828
|
+
.option("--expected-version <n>", "Lifecycle row version to replace (defaults to the current one)")
|
|
2829
|
+
.option("--json", "Print machine-readable JSON");
|
|
2830
|
+
if (status === "archived") {
|
|
2831
|
+
cmd.option("--confirm-nonempty", "Archive even when the Board still has stories");
|
|
2832
|
+
}
|
|
2833
|
+
cmd.action((projectId, opts) => wrap(() => runProjectLifecycle(projectId, status, opts))());
|
|
2834
|
+
}
|
|
2835
|
+
project
|
|
2836
|
+
.command("lifecycle [projectId]")
|
|
2837
|
+
.description("List completed and archived Boards, or show one project's lifecycle status")
|
|
2838
|
+
.requiredOption("--company <slug|uid>", "Company slug or cloud uid")
|
|
2839
|
+
.option("--json", "Print machine-readable JSON")
|
|
2840
|
+
.action((projectId, opts) => wrap(() => runProjectLifecycleList(projectId, opts))());
|
|
2693
2841
|
project
|
|
2694
2842
|
.command("member")
|
|
2695
2843
|
.description("Manage project members")
|
|
@@ -21,6 +21,7 @@ export declare function runMonitorEnabledCommand(deps?: {
|
|
|
21
21
|
writeError?: (text: string) => void;
|
|
22
22
|
}): Promise<number>;
|
|
23
23
|
export declare function armText(meta: MonitorMetadata): string;
|
|
24
|
+
export declare function expiredText(id: string, timeoutMs: number, events: number): string;
|
|
24
25
|
export declare function resolveMonitorStartTarget(hqRoot: string, discovered: MonitorTarget, explicitlyProvided: boolean, runtime?: MonitorRuntimeIdentity): MonitorTarget;
|
|
25
26
|
export declare function invokeLaneMonitorMessage(hqRoot: string, meta: MonitorMetadata, sequence: number, text: string, inProcess?: MonitorLaneMessageApi): {
|
|
26
27
|
delivered: boolean;
|
package/dist/commands/monitor.js
CHANGED
|
@@ -168,6 +168,9 @@ function formatDuration(milliseconds) {
|
|
|
168
168
|
return `${milliseconds / 1_000}s`;
|
|
169
169
|
return `${milliseconds}ms`;
|
|
170
170
|
}
|
|
171
|
+
export function expiredText(id, timeoutMs, events) {
|
|
172
|
+
return `[hq monitor ${id} expired after ${formatDuration(timeoutMs)} with ${events} events delivered. Re-arm with \`hq monitor start ... --timeout <duration>\` (for example --timeout 30m) or \`--persistent\` if you still need the watch — and widen the filter if silence was unexpected.]`;
|
|
173
|
+
}
|
|
171
174
|
function monitorTimeout(timeoutMs, onTimeout) {
|
|
172
175
|
const startedAt = performance.now();
|
|
173
176
|
let timer = null;
|
|
@@ -711,7 +714,7 @@ export async function runSupervisor(id, deps = {}) {
|
|
|
711
714
|
const priorEvents = state.event_count;
|
|
712
715
|
if (reason !== "stopped" && reason !== "target_ended" && reason !== "supervisor_died") {
|
|
713
716
|
const terminalText = reason === "expired"
|
|
714
|
-
?
|
|
717
|
+
? expiredText(id, meta.timeout_ms ?? MONITOR_DEFAULT_TIMEOUT_MS, priorEvents)
|
|
715
718
|
: reason === "rate_exceeded"
|
|
716
719
|
? `[hq monitor ${id} stopped: rate_exceeded after suppressing sustained output.]`
|
|
717
720
|
: `[hq monitor ${id} ${JSON.stringify(meta.description)} exited with code ${exitCode ?? 1} after ${priorEvents} events]`;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `hq repo add <path> --company <slug>` — register a git checkout with HQ:
|
|
4
|
+
* link it into `repos/<private|public>/<name>` and list it under
|
|
5
|
+
* `companies.<slug>.repos` in `companies/manifest.yaml`.
|
|
6
|
+
*
|
|
7
|
+
* The manifest edit is a line insert, not a YAML re-dump, so comments,
|
|
8
|
+
* ordering, and quoting in the rest of the file are left as they were. The
|
|
9
|
+
* result is parsed back before writing; if the entry does not show up where
|
|
10
|
+
* expected, nothing is written.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MANIFEST_RELATIVE_PATH: string;
|
|
13
|
+
export declare function manifestRepos(text: string, company: string): string[] | null;
|
|
14
|
+
/**
|
|
15
|
+
* Companies other than `company` whose `repos` list contains one of `entries`.
|
|
16
|
+
*/
|
|
17
|
+
export declare function otherCompaniesClaiming(text: string, company: string, entries: string[]): string[];
|
|
18
|
+
/**
|
|
19
|
+
* Return manifest text with `entry` appended to `companies.<company>.repos`.
|
|
20
|
+
* Returns the input unchanged when the entry is already listed. Throws when
|
|
21
|
+
* the company is not in the manifest or the edit cannot be placed. Pure.
|
|
22
|
+
*/
|
|
23
|
+
export declare function addRepoToManifestText(text: string, company: string, entry: string): string;
|
|
24
|
+
export interface RepoAddOptions {
|
|
25
|
+
source: string;
|
|
26
|
+
company: string;
|
|
27
|
+
hqRoot: string;
|
|
28
|
+
visibility: "private" | "public";
|
|
29
|
+
name?: string;
|
|
30
|
+
dryRun?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export interface RepoAddResult {
|
|
33
|
+
entry: string;
|
|
34
|
+
linked: boolean;
|
|
35
|
+
manifestChanged: boolean;
|
|
36
|
+
warnings: string[];
|
|
37
|
+
}
|
|
38
|
+
/** Register a checkout. Throws a user-facing Error on any refusal. */
|
|
39
|
+
export declare function runRepoAdd(opts: RepoAddOptions): RepoAddResult;
|
|
40
|
+
export declare function registerRepoCommand(program: Command): void;
|
|
41
|
+
//# sourceMappingURL=repo.d.ts.map
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as yaml from "js-yaml";
|
|
5
|
+
import { findHqRoot } from "../utils/manifest.js";
|
|
6
|
+
/**
|
|
7
|
+
* `hq repo add <path> --company <slug>` — register a git checkout with HQ:
|
|
8
|
+
* link it into `repos/<private|public>/<name>` and list it under
|
|
9
|
+
* `companies.<slug>.repos` in `companies/manifest.yaml`.
|
|
10
|
+
*
|
|
11
|
+
* The manifest edit is a line insert, not a YAML re-dump, so comments,
|
|
12
|
+
* ordering, and quoting in the rest of the file are left as they were. The
|
|
13
|
+
* result is parsed back before writing; if the entry does not show up where
|
|
14
|
+
* expected, nothing is written.
|
|
15
|
+
*/
|
|
16
|
+
export const MANIFEST_RELATIVE_PATH = path.join("companies", "manifest.yaml");
|
|
17
|
+
function indentOf(line) {
|
|
18
|
+
return line.length - line.trimStart().length;
|
|
19
|
+
}
|
|
20
|
+
function isContentLine(line) {
|
|
21
|
+
const t = line.trim();
|
|
22
|
+
return t !== "" && !t.startsWith("#");
|
|
23
|
+
}
|
|
24
|
+
function escapeRegExp(s) {
|
|
25
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
26
|
+
}
|
|
27
|
+
function parseManifest(text) {
|
|
28
|
+
const parsed = yaml.load(text);
|
|
29
|
+
if (!parsed || typeof parsed !== "object") {
|
|
30
|
+
throw new Error("companies/manifest.yaml is empty or not a mapping.");
|
|
31
|
+
}
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
export function manifestRepos(text, company) {
|
|
35
|
+
const doc = parseManifest(text);
|
|
36
|
+
const entry = doc.companies?.[company];
|
|
37
|
+
if (entry === undefined)
|
|
38
|
+
return null;
|
|
39
|
+
const repos = entry?.repos;
|
|
40
|
+
return Array.isArray(repos) ? repos.map(String) : [];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Companies other than `company` whose `repos` list contains one of `entries`.
|
|
44
|
+
*/
|
|
45
|
+
export function otherCompaniesClaiming(text, company, entries) {
|
|
46
|
+
const doc = parseManifest(text);
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const [slug, block] of Object.entries(doc.companies ?? {})) {
|
|
49
|
+
if (slug === company)
|
|
50
|
+
continue;
|
|
51
|
+
const repos = block?.repos;
|
|
52
|
+
if (Array.isArray(repos) && repos.map(String).some((r) => entries.includes(r))) {
|
|
53
|
+
out.push(slug);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Return manifest text with `entry` appended to `companies.<company>.repos`.
|
|
60
|
+
* Returns the input unchanged when the entry is already listed. Throws when
|
|
61
|
+
* the company is not in the manifest or the edit cannot be placed. Pure.
|
|
62
|
+
*/
|
|
63
|
+
export function addRepoToManifestText(text, company, entry) {
|
|
64
|
+
const before = manifestRepos(text, company);
|
|
65
|
+
if (before === null) {
|
|
66
|
+
throw new Error(`Company '${company}' is not in companies/manifest.yaml. Add the company first (/newcompany).`);
|
|
67
|
+
}
|
|
68
|
+
if (before.includes(entry))
|
|
69
|
+
return text;
|
|
70
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
71
|
+
const lines = text.split(/\r?\n/);
|
|
72
|
+
const companiesIdx = lines.findIndex((l) => /^companies:\s*(#.*)?$/.test(l));
|
|
73
|
+
if (companiesIdx === -1) {
|
|
74
|
+
throw new Error("companies/manifest.yaml has no top-level `companies:` key.");
|
|
75
|
+
}
|
|
76
|
+
const companyRe = new RegExp(`^(\\s+)(["']?)${escapeRegExp(company)}\\2:\\s*(#.*)?$`);
|
|
77
|
+
let companyIdx = -1;
|
|
78
|
+
for (let i = companiesIdx + 1; i < lines.length; i++) {
|
|
79
|
+
if (isContentLine(lines[i]) && indentOf(lines[i]) === 0)
|
|
80
|
+
break;
|
|
81
|
+
if (companyRe.test(lines[i])) {
|
|
82
|
+
companyIdx = i;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (companyIdx === -1) {
|
|
87
|
+
throw new Error(`Could not find the '${company}:' block in companies/manifest.yaml to edit.`);
|
|
88
|
+
}
|
|
89
|
+
const ci = indentOf(lines[companyIdx]);
|
|
90
|
+
let blockEnd = lines.length; // exclusive
|
|
91
|
+
for (let i = companyIdx + 1; i < lines.length; i++) {
|
|
92
|
+
if (isContentLine(lines[i]) && indentOf(lines[i]) <= ci) {
|
|
93
|
+
blockEnd = i;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
let lastContent = companyIdx;
|
|
98
|
+
for (let i = companyIdx + 1; i < blockEnd; i++) {
|
|
99
|
+
if (isContentLine(lines[i]))
|
|
100
|
+
lastContent = i;
|
|
101
|
+
}
|
|
102
|
+
for (let i = companyIdx + 1; i < blockEnd; i++) {
|
|
103
|
+
if (/^\s+repos:\s*\[\s*[^\]\s]/.test(lines[i]) && indentOf(lines[i]) > ci) {
|
|
104
|
+
throw new Error(`companies.${company}.repos is an inline list (repos: [...]), which this command does not edit. Add the entry by hand.`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const out = [...lines];
|
|
108
|
+
const reposIdx = (() => {
|
|
109
|
+
for (let i = companyIdx + 1; i < blockEnd; i++) {
|
|
110
|
+
if (/^\s+repos:\s*(\[\s*\])?\s*(#.*)?$/.test(lines[i]) && indentOf(lines[i]) > ci) {
|
|
111
|
+
return i;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return -1;
|
|
115
|
+
})();
|
|
116
|
+
if (reposIdx === -1) {
|
|
117
|
+
const keyIndent = " ".repeat(ci + 2);
|
|
118
|
+
out.splice(lastContent + 1, 0, `${keyIndent}repos:`, `${keyIndent} - ${entry}`);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const ri = indentOf(lines[reposIdx]);
|
|
122
|
+
if (/\[\s*\]/.test(lines[reposIdx])) {
|
|
123
|
+
out[reposIdx] = `${" ".repeat(ri)}repos:`;
|
|
124
|
+
out.splice(reposIdx + 1, 0, `${" ".repeat(ri + 2)}- ${entry}`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
let lastItem = -1;
|
|
128
|
+
let itemPrefix = `${" ".repeat(ri + 2)}- `;
|
|
129
|
+
for (let i = reposIdx + 1; i < blockEnd; i++) {
|
|
130
|
+
const l = lines[i];
|
|
131
|
+
if (!isContentLine(l))
|
|
132
|
+
continue;
|
|
133
|
+
const ind = indentOf(l);
|
|
134
|
+
if (ind < ri || (ind === ri && !l.trimStart().startsWith("- ")))
|
|
135
|
+
break;
|
|
136
|
+
if (l.trimStart().startsWith("- ")) {
|
|
137
|
+
lastItem = i;
|
|
138
|
+
itemPrefix = `${" ".repeat(ind)}- `;
|
|
139
|
+
}
|
|
140
|
+
else if (ind <= ri) {
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
out.splice((lastItem === -1 ? reposIdx : lastItem) + 1, 0, `${itemPrefix}${entry}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const next = out.join(eol);
|
|
148
|
+
const after = manifestRepos(next, company);
|
|
149
|
+
if (!after || !after.includes(entry) || after.length !== before.length + 1) {
|
|
150
|
+
throw new Error("Could not update companies/manifest.yaml safely (unusual layout). Add the entry by hand.");
|
|
151
|
+
}
|
|
152
|
+
return next;
|
|
153
|
+
}
|
|
154
|
+
function isInside(parent, child) {
|
|
155
|
+
const rel = path.relative(parent, child);
|
|
156
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Write `text` to `file` through a temp file in the same directory and a
|
|
160
|
+
* rename, so a crash cannot leave a truncated manifest. Refuses when the file
|
|
161
|
+
* no longer holds `expected` (another writer edited it since it was read).
|
|
162
|
+
*/
|
|
163
|
+
function writeManifestAtomically(file, expected, text) {
|
|
164
|
+
const tmp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
165
|
+
fs.writeFileSync(tmp, text);
|
|
166
|
+
try {
|
|
167
|
+
if (fs.readFileSync(file, "utf8") !== expected) {
|
|
168
|
+
throw new Error("companies/manifest.yaml changed while this command was running. Nothing was written to it; run the command again.");
|
|
169
|
+
}
|
|
170
|
+
fs.renameSync(tmp, file);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
fs.rmSync(tmp, { force: true });
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function isGitCheckout(dir) {
|
|
178
|
+
return fs.existsSync(path.join(dir, ".git"));
|
|
179
|
+
}
|
|
180
|
+
/** Register a checkout. Throws a user-facing Error on any refusal. */
|
|
181
|
+
export function runRepoAdd(opts) {
|
|
182
|
+
const hqRoot = path.resolve(opts.hqRoot);
|
|
183
|
+
const manifestPath = path.join(hqRoot, MANIFEST_RELATIVE_PATH);
|
|
184
|
+
if (!fs.existsSync(manifestPath)) {
|
|
185
|
+
throw new Error(`No companies/manifest.yaml under ${hqRoot}. Pass --hq-root <path>.`);
|
|
186
|
+
}
|
|
187
|
+
const source = path.resolve(opts.source);
|
|
188
|
+
if (!fs.existsSync(source) || !fs.statSync(source).isDirectory()) {
|
|
189
|
+
throw new Error(`${source} is not a directory.`);
|
|
190
|
+
}
|
|
191
|
+
if (!isGitCheckout(source)) {
|
|
192
|
+
throw new Error(`${source} is not a git checkout (no .git).`);
|
|
193
|
+
}
|
|
194
|
+
const sourceReal = fs.realpathSync(source);
|
|
195
|
+
const hqRootReal = fs.realpathSync(hqRoot);
|
|
196
|
+
const reposRoot = fs.existsSync(path.join(hqRoot, "repos"))
|
|
197
|
+
? fs.realpathSync(path.join(hqRoot, "repos"))
|
|
198
|
+
: path.join(hqRootReal, "repos");
|
|
199
|
+
// Direct child of repos/private or repos/public, checked on the path as
|
|
200
|
+
// given (parent resolved, last component not), so an existing link such as
|
|
201
|
+
// repos/private/foo -> ~/code/bar is registered as itself, not re-linked.
|
|
202
|
+
const directChild = (p) => {
|
|
203
|
+
const rel = path.relative(reposRoot, p);
|
|
204
|
+
const parts = rel.split(path.sep);
|
|
205
|
+
if (!rel.startsWith("..") &&
|
|
206
|
+
!path.isAbsolute(rel) &&
|
|
207
|
+
parts.length === 2 &&
|
|
208
|
+
(parts[0] === "private" || parts[0] === "public")) {
|
|
209
|
+
return parts;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
};
|
|
213
|
+
const sourceAsGiven = path.join(fs.realpathSync(path.dirname(source)), path.basename(source));
|
|
214
|
+
const inPlace = directChild(sourceAsGiven) ?? directChild(sourceReal);
|
|
215
|
+
if (!inPlace && isInside(hqRootReal, sourceReal)) {
|
|
216
|
+
throw new Error(`${source} is inside the HQ root. Only a checkout directly under repos/private or repos/public can be registered from inside HQ.`);
|
|
217
|
+
}
|
|
218
|
+
const warnings = [];
|
|
219
|
+
let entry;
|
|
220
|
+
let linkPath = null;
|
|
221
|
+
if (inPlace) {
|
|
222
|
+
entry = ["repos", ...inPlace].join("/");
|
|
223
|
+
if (opts.name !== undefined) {
|
|
224
|
+
warnings.push(`--name is ignored: ${entry} is already under repos/ and is registered in place.`);
|
|
225
|
+
}
|
|
226
|
+
if (opts.visibility === "public" && inPlace[0] !== "public") {
|
|
227
|
+
warnings.push(`--public is ignored: ${entry} is already under repos/ and is registered in place.`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
const name = opts.name ?? path.basename(sourceReal);
|
|
232
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name) || name === "." || name === "..") {
|
|
233
|
+
throw new Error(`'${name}' is not a usable repo name. Pass --name <name>.`);
|
|
234
|
+
}
|
|
235
|
+
entry = `repos/${opts.visibility}/${name}`;
|
|
236
|
+
linkPath = path.join(hqRoot, "repos", opts.visibility, name);
|
|
237
|
+
if (fs.existsSync(linkPath) || isDanglingSymlink(linkPath)) {
|
|
238
|
+
const existingReal = safeRealpath(linkPath);
|
|
239
|
+
if (existingReal !== sourceReal) {
|
|
240
|
+
throw new Error(`${entry} already exists and points somewhere else. Pass --name <name> to pick another name.`);
|
|
241
|
+
}
|
|
242
|
+
linkPath = null; // already linked to this checkout
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
const manifestText = fs.readFileSync(manifestPath, "utf8");
|
|
246
|
+
const nextText = addRepoToManifestText(manifestText, opts.company, entry);
|
|
247
|
+
const manifestChanged = nextText !== manifestText;
|
|
248
|
+
// Refuse when another company already lists this checkout. Consumers treat a
|
|
249
|
+
// repo claimed by two companies as ambiguous, so a second claim would
|
|
250
|
+
// silently drop the first company's ownership.
|
|
251
|
+
const parsed = parseManifest(manifestText);
|
|
252
|
+
const sameCheckout = new Set([entry]);
|
|
253
|
+
for (const block of Object.values(parsed.companies ?? {})) {
|
|
254
|
+
const repos = block?.repos;
|
|
255
|
+
if (!Array.isArray(repos))
|
|
256
|
+
continue;
|
|
257
|
+
for (const r of repos.map(String)) {
|
|
258
|
+
if (safeRealpath(path.resolve(hqRoot, r)) === sourceReal)
|
|
259
|
+
sameCheckout.add(r);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const claimants = otherCompaniesClaiming(manifestText, opts.company, [...sameCheckout]);
|
|
263
|
+
if (claimants.length > 0) {
|
|
264
|
+
throw new Error(`${entry} is already listed for ${claimants.join(", ")} in companies/manifest.yaml. A repo can belong to one company; remove it there first.`);
|
|
265
|
+
}
|
|
266
|
+
if (!opts.dryRun) {
|
|
267
|
+
if (linkPath) {
|
|
268
|
+
fs.mkdirSync(path.dirname(linkPath), { recursive: true });
|
|
269
|
+
fs.symlinkSync(sourceReal, linkPath, "dir");
|
|
270
|
+
}
|
|
271
|
+
if (manifestChanged)
|
|
272
|
+
writeManifestAtomically(manifestPath, manifestText, nextText);
|
|
273
|
+
}
|
|
274
|
+
return { entry, linked: linkPath !== null, manifestChanged, warnings };
|
|
275
|
+
}
|
|
276
|
+
function isDanglingSymlink(p) {
|
|
277
|
+
try {
|
|
278
|
+
return fs.lstatSync(p).isSymbolicLink();
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function safeRealpath(p) {
|
|
285
|
+
try {
|
|
286
|
+
return fs.realpathSync(p);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
export function registerRepoCommand(program) {
|
|
293
|
+
const repo = program
|
|
294
|
+
.command("repo")
|
|
295
|
+
.description("Register code repos with HQ (repos/ and companies/manifest.yaml)");
|
|
296
|
+
repo
|
|
297
|
+
.command("add <path>")
|
|
298
|
+
.description("Link a git checkout into repos/<private|public>/<name> and list it under " +
|
|
299
|
+
"companies.<slug>.repos in companies/manifest.yaml. A checkout already " +
|
|
300
|
+
"directly under repos/private or repos/public is registered in place.")
|
|
301
|
+
.requiredOption("--company <slug>", "Company that owns the repo (checked against companies/manifest.yaml only, not the session's locked company set or your memberships)")
|
|
302
|
+
.option("--public", "Link under repos/public (default: repos/private)")
|
|
303
|
+
.option("--name <name>", "Name under repos/ (default: the checkout's folder name)")
|
|
304
|
+
.option("--hq-root <path>", "HQ root (default: found from the current directory)")
|
|
305
|
+
.option("--dry-run", "Print what would change without changing anything")
|
|
306
|
+
.action((source, opts) => {
|
|
307
|
+
try {
|
|
308
|
+
const result = runRepoAdd({
|
|
309
|
+
source,
|
|
310
|
+
company: opts.company,
|
|
311
|
+
hqRoot: opts.hqRoot ?? findHqRoot(),
|
|
312
|
+
visibility: opts.public ? "public" : "private",
|
|
313
|
+
name: opts.name,
|
|
314
|
+
dryRun: opts.dryRun,
|
|
315
|
+
});
|
|
316
|
+
for (const w of result.warnings)
|
|
317
|
+
console.warn(chalk.yellow("Warning:"), w);
|
|
318
|
+
const prefix = opts.dryRun ? "Would register" : "Registered";
|
|
319
|
+
const parts = [
|
|
320
|
+
result.linked ? "linked" : "already linked",
|
|
321
|
+
result.manifestChanged ? "added to the manifest" : "already in the manifest",
|
|
322
|
+
];
|
|
323
|
+
console.log(chalk.green(`${prefix} ${result.entry} for ${opts.company} (${parts.join(", ")}).`));
|
|
324
|
+
}
|
|
325
|
+
catch (err) {
|
|
326
|
+
console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err));
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
//# sourceMappingURL=repo.js.map
|
package/dist/commands/scan.d.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* hq scan secrets --json findings as JSON
|
|
7
7
|
*
|
|
8
8
|
* Rules: the vendored gitleaks default rule set (MIT, vendor/gitleaks/) plus
|
|
9
|
-
* HQ's own token patterns. No network, no account, no quota.
|
|
9
|
+
* HQ's own token patterns. No network, no account, no quota. A staged scan also
|
|
10
|
+
* honours `[[allowlists]]` / `[allowlist]` in the repo's own .gitleaks.toml
|
|
11
|
+
* (lib/secret-scan/repo-config.ts); a file it cannot accept exits 2.
|
|
10
12
|
*
|
|
11
13
|
* Exit codes: 0 clean, 1 possible secret found, 2 the scan could not run.
|
|
12
14
|
* A pre-commit hook blocks on any non-zero exit, so the gate fails closed.
|
package/dist/commands/scan.js
CHANGED
|
@@ -6,12 +6,15 @@
|
|
|
6
6
|
* hq scan secrets --json findings as JSON
|
|
7
7
|
*
|
|
8
8
|
* Rules: the vendored gitleaks default rule set (MIT, vendor/gitleaks/) plus
|
|
9
|
-
* HQ's own token patterns. No network, no account, no quota.
|
|
9
|
+
* HQ's own token patterns. No network, no account, no quota. A staged scan also
|
|
10
|
+
* honours `[[allowlists]]` / `[allowlist]` in the repo's own .gitleaks.toml
|
|
11
|
+
* (lib/secret-scan/repo-config.ts); a file it cannot accept exits 2.
|
|
10
12
|
*
|
|
11
13
|
* Exit codes: 0 clean, 1 possible secret found, 2 the scan could not run.
|
|
12
14
|
* A pre-commit hook blocks on any non-zero exit, so the gate fails closed.
|
|
13
15
|
* Output names the rule, file and line; secret values are never printed.
|
|
14
16
|
*/
|
|
17
|
+
import { ScanConfigError } from "../lib/secret-scan/repo-config.js";
|
|
15
18
|
import { ScanInputError, scanFiles, scanStaged } from "../lib/secret-scan/staged.js";
|
|
16
19
|
function report(findings, scope) {
|
|
17
20
|
if (!findings.length)
|
|
@@ -22,7 +25,8 @@ function report(findings, scope) {
|
|
|
22
25
|
process.stderr.write(` ${f.file}:${f.line} ${f.ruleId} ${f.description}\n`);
|
|
23
26
|
}
|
|
24
27
|
process.stderr.write("\nRemove the secret from the change, then stage it again. Keep real credentials in the HQ vault\n" +
|
|
25
|
-
"(hq secrets, hq run). A known false positive can carry a `gitleaks:allow` comment on that line
|
|
28
|
+
"(hq secrets, hq run). A known false positive can carry a `gitleaks:allow` comment on that line,\n" +
|
|
29
|
+
"or an [[allowlists]] entry in the repo's .gitleaks.toml.\n");
|
|
26
30
|
}
|
|
27
31
|
export function runScanSecrets(paths, opts, cwd = process.cwd()) {
|
|
28
32
|
let findings;
|
|
@@ -30,7 +34,7 @@ export function runScanSecrets(paths, opts, cwd = process.cwd()) {
|
|
|
30
34
|
findings = paths.length && !opts.staged ? scanFiles(paths) : scanStaged(cwd);
|
|
31
35
|
}
|
|
32
36
|
catch (err) {
|
|
33
|
-
const msg = err instanceof ScanInputError ? err.message : `scan failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
37
|
+
const msg = err instanceof ScanInputError || err instanceof ScanConfigError ? err.message : `scan failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
34
38
|
if (opts.json)
|
|
35
39
|
process.stdout.write(`${JSON.stringify({ ok: false, error: msg })}\n`);
|
|
36
40
|
process.stderr.write(`HQ secret scan: ${msg}\n`);
|
|
@@ -20,9 +20,21 @@ export { vaultApiFetch, getCompanyUid, getEntityUid };
|
|
|
20
20
|
*/
|
|
21
21
|
export declare const HQ_PRO_RESOURCE_SURFACES: readonly ["api", "bot_invite", "bot_join_now", "calendar_auto_schedule", "calendar_preferences", "cli_secrets_set", "console_secrets_form", "deploy_precheck", "external_connection", "factory_install", "integrations_connect", "oauth_callback", "plan_limit_check", "secrets_input_link", "web"];
|
|
22
22
|
export declare const CLI_SECRETS_SET_SURFACE = "cli_secrets_set";
|
|
23
|
+
/**
|
|
24
|
+
* Shown when `secrets share` / `unshare` gets a 403. Changing a secret's
|
|
25
|
+
* grants needs admin on that secret, which is a separate check from read.
|
|
26
|
+
* The old "Not authorized to share this secret" text led people and agents to
|
|
27
|
+
* conclude the secret itself was locked to them, so this names the missing
|
|
28
|
+
* permission and says read access is unchanged.
|
|
29
|
+
*/
|
|
30
|
+
export declare const SECRET_SHARE_ADMIN_REQUIRED_MESSAGE: string;
|
|
23
31
|
interface SecretAclPrincipal {
|
|
24
32
|
granteeType: "email" | "group" | "company-wide" | "person";
|
|
25
33
|
granteeId: string;
|
|
34
|
+
/** What the caller typed, when it differs from the wire grantee (agt_… uids). */
|
|
35
|
+
label?: string;
|
|
36
|
+
/** The fleet agent behind an agt_… uid or its machine email. */
|
|
37
|
+
agentUid?: string;
|
|
26
38
|
}
|
|
27
39
|
/**
|
|
28
40
|
* hq-pro refuses any Outpost grant above read with 400
|
|
@@ -31,6 +43,15 @@ interface SecretAclPrincipal {
|
|
|
31
43
|
export declare const OUTPOST_GRANT_READ_ONLY_CODE = "OUTPOST_GRANT_READ_ONLY";
|
|
32
44
|
export declare const OUTPOST_GRANT_READ_ONLY_MESSAGE = "Company Outposts can only be given read access to a secret.";
|
|
33
45
|
export declare function parseSecretAclPrincipal(principal: string): SecretAclPrincipal | null;
|
|
46
|
+
/**
|
|
47
|
+
* Refuse a share to a fleet agent that is not an active agent of this
|
|
48
|
+
* company. hq-pro turns the machine-email grantee into an agt_… grant only
|
|
49
|
+
* for an active member; for any other agent it stores an email row the agent
|
|
50
|
+
* can never use and still returns 200. Reads the company roster
|
|
51
|
+
* (`GET /v1/agents?companyUid=…`, the same call as `hq agents list`) before
|
|
52
|
+
* any write. Returns an error message, or null when the agent is active here.
|
|
53
|
+
*/
|
|
54
|
+
export declare function inactiveCompanyAgentReason(token: string, companyUid: string, agentUid: string): Promise<string | null>;
|
|
34
55
|
export interface SecretInjectionRecipe {
|
|
35
56
|
header: string;
|
|
36
57
|
scheme: "raw" | "bearer";
|