quoxcli 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/README.md +2 -2
- package/bin/quox.js +19 -1
- package/bin/quoxon.js +6 -1
- package/lib/api/endpoints/admin.js +7 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/governance.js +7 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +58 -0
- package/lib/api/endpoints/index.js +4 -0
- package/lib/api/endpoints/lucid.js +17 -0
- package/lib/api/endpoints/mcp.js +35 -0
- package/lib/api/endpoints/mon.js +122 -0
- package/lib/api/endpoints/notesq.js +2 -0
- package/lib/api/endpoints/quoxplan.js +51 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +13 -0
- package/lib/commands/actions.js +51 -13
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agentAttach.js +15 -0
- package/lib/commands/approvals.js +193 -3
- package/lib/commands/ask.js +1 -1
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +15 -0
- package/lib/commands/claude.js +4 -1
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/contacts.js +2 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/evidence.js +2 -1
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +78 -5
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +30 -3
- package/lib/commands/lucid.js +7 -0
- package/lib/commands/lucidGoals.js +389 -0
- package/lib/commands/mail.js +427 -25
- package/lib/commands/mcp.js +190 -0
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +69 -144
- package/lib/commands/monitoring.js +68 -0
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +617 -0
- package/lib/commands/objective.js +5 -2
- package/lib/commands/offload.js +52 -12
- package/lib/commands/plan.js +487 -52
- package/lib/commands/provider.js +29 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/run.js +2 -1
- package/lib/commands/server.js +37 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/skills.js +4 -1
- package/lib/commands/slot.js +49 -9
- package/lib/commands/status.js +92 -44
- package/lib/commands/tool.js +30 -0
- package/lib/commands/vault.js +6 -3
- package/lib/commands/volt.js +6 -1
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +880 -0
- package/lib/commands/wazuhIntake.js +358 -0
- package/lib/commands/wazuhReport.js +192 -0
- package/lib/commands/workflow.js +47 -4
- package/lib/commands/x.js +50 -0
- package/lib/core/announce.js +86 -0
- package/lib/core/auth.js +20 -2
- package/lib/core/callMe.js +118 -0
- package/lib/core/client.js +27 -1
- package/lib/core/commandRegistrars.js +9 -0
- package/lib/core/config.js +42 -3
- package/lib/core/confirm.js +68 -7
- package/lib/core/crossOrgApprovals.js +69 -0
- package/lib/core/errors.js +52 -1
- package/lib/core/governedSession.js +135 -5
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/offload.js +120 -0
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +30 -5
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/selfUpdate.js +2 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/walkthrough.js +9 -5
- package/lib/dragon/select.js +2 -2
- package/lib/quoxon/EXTENSIONS.md +142 -19
- package/lib/quoxon/api.js +189 -34
- package/lib/quoxon/app.js +1277 -188
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +56 -2
- package/lib/quoxon/approvalSentence.js +8 -4
- package/lib/quoxon/approvalsView.js +72 -12
- package/lib/quoxon/ask.js +4 -2
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +8 -2
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +8 -1
- package/lib/quoxon/commands.js +42 -12
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/delegationDragon.js +49 -0
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dnd.js +118 -0
- package/lib/quoxon/dragonCompanion.js +37 -7
- package/lib/quoxon/dragonMoments.js +78 -0
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +55 -8
- package/lib/quoxon/extensions.js +400 -38
- package/lib/quoxon/foreignText.js +49 -0
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +125 -22
- package/lib/quoxon/keys.js +2 -2
- package/lib/quoxon/launcher.js +2 -1
- package/lib/quoxon/loopCard.js +7 -2
- package/lib/quoxon/loopCards.js +17 -8
- package/lib/quoxon/loopStream.js +20 -9
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +27 -6
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/onboarding/connectServer.js +162 -0
- package/lib/quoxon/onboarding/copy.js +6 -0
- package/lib/quoxon/onboarding/dockerOffer.js +3 -1
- package/lib/quoxon/onboarding/index.js +5 -43
- package/lib/quoxon/onboarding/probe.js +20 -1
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +22 -0
- package/lib/quoxon/print.js +2 -0
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/render.js +68 -16
- package/lib/quoxon/resend.js +14 -0
- package/lib/quoxon/roster.js +10 -5
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/rows.js +3 -1
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +88 -0
- package/lib/quoxon/slash/agents.js +2 -1
- package/lib/quoxon/slash/argSources.js +9 -2
- package/lib/quoxon/slash/args.js +4 -2
- package/lib/quoxon/slash/broadcast.js +48 -11
- package/lib/quoxon/slash/broadcastRun.js +24 -7
- package/lib/quoxon/slash/cliLeaves.json +98 -1
- package/lib/quoxon/slash/dashboard.js +2 -0
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/listView.js +2 -1
- package/lib/quoxon/slash/palette.js +63 -1
- package/lib/quoxon/slash/paletteController.js +15 -5
- package/lib/quoxon/slash/restart.js +9 -4
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slashSpinner.js +35 -0
- package/lib/quoxon/slotAttach.js +8 -0
- package/lib/quoxon/slotChat.js +24 -3
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/slotRun.js +2 -0
- package/lib/quoxon/suggestCommand.js +1 -1
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/transcript.js +2 -1
- package/lib/quoxon/webPty.js +15 -0
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +42 -0
- package/lib/render/planBrief.js +614 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +61 -3
- package/package.json +3 -2
- package/vendor/dragonlab/SOURCE +3 -3
- package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
- package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
- package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
- package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
- package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
- package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
- package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
- package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
- package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
- package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +20 -0
- package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +1 -1
- package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
|
@@ -29,6 +29,19 @@ export const tools = {
|
|
|
29
29
|
path: () => `/api/tools/install-to-scope`,
|
|
30
30
|
tags: ['live-write'],
|
|
31
31
|
},
|
|
32
|
+
// CLI-COV 2026-11-05: read and undo what `tool install` wrote (tool_installations, not the org's tool definitions)
|
|
33
|
+
'tools.installed': {
|
|
34
|
+
method: 'GET',
|
|
35
|
+
service: 'auth',
|
|
36
|
+
path: () => `/api/tools/installed`,
|
|
37
|
+
tags: ['live-safe'],
|
|
38
|
+
},
|
|
39
|
+
'tools.uninstall-from-scope': {
|
|
40
|
+
method: 'DELETE',
|
|
41
|
+
service: 'auth',
|
|
42
|
+
path: ({ toolId }) => `/api/tools/install-to-scope/${encodeURIComponent(toolId)}`,
|
|
43
|
+
tags: ['live-write'],
|
|
44
|
+
},
|
|
32
45
|
'tools.list': {
|
|
33
46
|
method: 'GET',
|
|
34
47
|
service: 'auth',
|
package/lib/api/endpoints/v1.js
CHANGED
|
@@ -365,6 +365,19 @@ export const v1 = {
|
|
|
365
365
|
path: ({ id }) => `/api/v1/approvals/grants/${encodeURIComponent(id)}/revoke`,
|
|
366
366
|
tags: ['live-write'],
|
|
367
367
|
},
|
|
368
|
+
// ApprovalsQ call-me policy (2026-11-03): human org admin only; flips the stale-approval phone watcher.
|
|
369
|
+
'v1.approvals.call-me.get': {
|
|
370
|
+
method: 'GET',
|
|
371
|
+
service: 'collector',
|
|
372
|
+
path: () => `/api/v1/approvalsq/call-me`,
|
|
373
|
+
tags: ['live-safe'],
|
|
374
|
+
},
|
|
375
|
+
'v1.approvals.call-me.put': {
|
|
376
|
+
method: 'PUT',
|
|
377
|
+
service: 'collector',
|
|
378
|
+
path: () => `/api/v1/approvalsq/call-me`,
|
|
379
|
+
tags: ['live-write'],
|
|
380
|
+
},
|
|
368
381
|
'v1.tools.list': {
|
|
369
382
|
method: 'GET',
|
|
370
383
|
service: 'collector',
|
package/lib/commands/actions.js
CHANGED
|
@@ -41,6 +41,39 @@ async function listItems(ctx, { status = 'all', limit = 100, scope } = {}) {
|
|
|
41
41
|
throw new QuoxError(`More than ${MAX_PAGES * pageSize} items; narrow with --status or --scope.`);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
// Short ids, as `quox approvals` shows them: the last 6 characters of the ULID. --json keeps the full id.
|
|
45
|
+
const SHORT_LEN = 6;
|
|
46
|
+
export const shortActionId = (id) => String(id ?? '').slice(-SHORT_LEN);
|
|
47
|
+
|
|
48
|
+
// The width to fit: the terminal when there is one, else COLUMNS (the Quox TUI sets it for ! and /<verb> runs).
|
|
49
|
+
function outputWidth() {
|
|
50
|
+
if (process.stdout.isTTY && process.stdout.columns) return process.stdout.columns;
|
|
51
|
+
const env = Number(process.env.COLUMNS);
|
|
52
|
+
return env > 0 ? env : 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** An exact id, or a unique id suffix (the short id the list shows). -> item | null; ambiguous throws. */
|
|
56
|
+
function findByIdOrSuffix(items, given) {
|
|
57
|
+
const want = String(given || '').trim().toUpperCase();
|
|
58
|
+
if (!want) return null;
|
|
59
|
+
const exact = items.find((i) => String(i.id).toUpperCase() === want);
|
|
60
|
+
if (exact) return exact;
|
|
61
|
+
const hits = items.filter((i) => String(i.id).toUpperCase().endsWith(want));
|
|
62
|
+
if (hits.length > 1) {
|
|
63
|
+
throw new QuoxError(`"${given}" matches ${hits.length} action items (${hits.map((h) => h.id).join(', ')}); use more of the id.`, 2);
|
|
64
|
+
}
|
|
65
|
+
return hits[0] || null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// A short id (base32, shorter than a ULID) is resolved against the list; anything else goes to the server as given.
|
|
69
|
+
const looksShort = (id) => /^[0-9A-Z]{4,25}$/i.test(String(id || '').trim());
|
|
70
|
+
async function resolveActionId(ctx, given) {
|
|
71
|
+
if (!looksShort(given)) return given;
|
|
72
|
+
const item = findByIdOrSuffix(await listItems(ctx, { status: 'all', limit: 100 }), given);
|
|
73
|
+
if (!item) throw new QuoxError(`No action item matches ${given}.`, 5);
|
|
74
|
+
return item.id;
|
|
75
|
+
}
|
|
76
|
+
|
|
44
77
|
export function registerActionsCommands(program, ctx) {
|
|
45
78
|
const actions = program
|
|
46
79
|
.command('actions')
|
|
@@ -54,20 +87,24 @@ export function registerActionsCommands(program, ctx) {
|
|
|
54
87
|
.option('--scope <scopeId>', 'only items in this team/department/division scope')
|
|
55
88
|
.action(async (options) => {
|
|
56
89
|
const items = await listItems({ ...ctx, program }, options);
|
|
90
|
+
// Fit the terminal (owner 2026-11-07): short id, title cut with an ellipsis, no all-empty Due column.
|
|
91
|
+
const hasDue = items.some((i) => i.dueDate);
|
|
92
|
+
const columns = ['id', 'status', 'priority', 'title', ...(hasDue ? ['dueDate'] : [])];
|
|
93
|
+
const headers = ['ID', 'Status', 'Priority', 'Title', ...(hasDue ? ['Due'] : [])];
|
|
57
94
|
formatOutput({
|
|
58
|
-
columns
|
|
59
|
-
headers
|
|
60
|
-
rows: items.map((i) => ({ ...i, dueDate: i.dueDate
|
|
61
|
-
}, { raw: items });
|
|
95
|
+
columns,
|
|
96
|
+
headers,
|
|
97
|
+
rows: items.map((i) => ({ ...i, fullId: i.id, id: shortActionId(i.id), dueDate: i.dueDate ? String(i.dueDate).slice(0, 10) : '' })),
|
|
98
|
+
}, { raw: items, quietColumn: 'fullId', fit: { width: outputWidth(), flex: 'title' } });
|
|
62
99
|
});
|
|
63
100
|
|
|
64
101
|
actions
|
|
65
102
|
.command('get')
|
|
66
103
|
.description('Show one action item by id')
|
|
67
|
-
.argument('<id>', 'action item id
|
|
104
|
+
.argument('<id>', 'action item id, or the short id `quox actions list` shows')
|
|
68
105
|
.action(async (id) => {
|
|
69
106
|
const items = await listItems({ ...ctx, program }, { status: 'all', limit: 100 });
|
|
70
|
-
const item = items
|
|
107
|
+
const item = findByIdOrSuffix(items, id);
|
|
71
108
|
if (!item) throw new QuoxError(`No action item ${id} among the latest 100 items.`);
|
|
72
109
|
formatDetail(item);
|
|
73
110
|
});
|
|
@@ -90,10 +127,10 @@ export function registerActionsCommands(program, ctx) {
|
|
|
90
127
|
actions
|
|
91
128
|
.command('done')
|
|
92
129
|
.description('Mark an Action Tracker item done')
|
|
93
|
-
.argument('<id>', 'action item id
|
|
130
|
+
.argument('<id>', 'action item id, or the short id `quox actions list` shows')
|
|
94
131
|
.action(async (id) => {
|
|
95
132
|
ctx.auth.requireAuth();
|
|
96
|
-
const item = await completeActionItem(ctx.client, id);
|
|
133
|
+
const item = await completeActionItem(ctx.client, await resolveActionId({ ...ctx, program }, id));
|
|
97
134
|
if (program.opts().output === 'json') return formatDetail(item);
|
|
98
135
|
printSuccess(`Marked done ${item.id}: ${item.title}`);
|
|
99
136
|
return undefined;
|
|
@@ -102,11 +139,11 @@ export function registerActionsCommands(program, ctx) {
|
|
|
102
139
|
actions
|
|
103
140
|
.command('move')
|
|
104
141
|
.description('Move an Action Tracker item to todo, doing or done')
|
|
105
|
-
.argument('<id>', 'action item id
|
|
142
|
+
.argument('<id>', 'action item id, or the short id `quox actions list` shows')
|
|
106
143
|
.argument('<status>', STATUS_COLUMNS.join(', '))
|
|
107
144
|
.action(async (id, status) => {
|
|
108
145
|
ctx.auth.requireAuth();
|
|
109
|
-
const item = await moveActionItem(ctx.client, id, status);
|
|
146
|
+
const item = await moveActionItem(ctx.client, await resolveActionId({ ...ctx, program }, id), status);
|
|
110
147
|
if (program.opts().output === 'json') return formatDetail(item);
|
|
111
148
|
printSuccess(`Moved ${item.id} to ${item.status}: ${item.title}`);
|
|
112
149
|
return undefined;
|
|
@@ -115,15 +152,16 @@ export function registerActionsCommands(program, ctx) {
|
|
|
115
152
|
actions
|
|
116
153
|
.command('delete')
|
|
117
154
|
.description('Delete one of your Action Tracker items (asks first; --yes skips, required without a terminal)')
|
|
118
|
-
.argument('<id>', 'action item id
|
|
155
|
+
.argument('<id>', 'action item id, or the short id `quox actions list` shows')
|
|
119
156
|
.option('-y, --yes', 'do not ask')
|
|
120
157
|
.action(async (id, options) => {
|
|
121
158
|
ctx.auth.requireAuth();
|
|
122
|
-
|
|
159
|
+
const fullId = await resolveActionId({ ...ctx, program }, id);
|
|
160
|
+
if (!(await confirmDestructive(program, { force: options.yes === true }, `Delete action item ${fullId}?`))) {
|
|
123
161
|
console.log('Not deleted.');
|
|
124
162
|
return undefined;
|
|
125
163
|
}
|
|
126
|
-
const r = await deleteActionItem(ctx.client,
|
|
164
|
+
const r = await deleteActionItem(ctx.client, fullId);
|
|
127
165
|
if (program.opts().output === 'json') return formatDetail(r);
|
|
128
166
|
printSuccess(`Deleted ${r.id}${r.title ? `: ${r.title}` : ''}`);
|
|
129
167
|
return undefined;
|
package/lib/commands/admin.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { formatOutput, formatDetail, printSuccess } from '../core/output.js';
|
|
6
6
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
7
|
+
import { withStepUp } from '../core/stepUpPrompt.js';
|
|
8
|
+
import { announce, announceFailure } from '../core/announce.js';
|
|
9
|
+
import { resolveOrgId } from './helpers.js';
|
|
7
10
|
|
|
8
11
|
export function registerAdminCommands(program, ctx) {
|
|
9
12
|
const { client, auth } = ctx;
|
|
@@ -154,6 +157,38 @@ export function registerAdminCommands(program, ctx) {
|
|
|
154
157
|
console.log(data.token);
|
|
155
158
|
});
|
|
156
159
|
|
|
160
|
+
// ADMIN-VERIFY-EMAIL: accounts with no real mailbox (test/service accounts) can never click the
|
|
161
|
+
// emailed link, so they never see org invites. Instance-admin, human, step-up PIN (hidden prompt).
|
|
162
|
+
const users = admin
|
|
163
|
+
.command('users')
|
|
164
|
+
.description('Instance user administration');
|
|
165
|
+
|
|
166
|
+
users
|
|
167
|
+
.command('verify-email')
|
|
168
|
+
.description('Mark a user email verified (instance admin; asks for your step-up PIN; audited)')
|
|
169
|
+
.argument('<email>', 'the email address to mark verified')
|
|
170
|
+
.requiredOption('--reason <text>', 'why this address is verified without a mailbox (recorded in the audit log)')
|
|
171
|
+
.option('--user-id <id>', 'refuse unless the email belongs to this user id')
|
|
172
|
+
.action(async (email, opts) => {
|
|
173
|
+
auth.requireAuth();
|
|
174
|
+
const reason = String(opts.reason || '').trim();
|
|
175
|
+
if (!reason) throw new Error('--reason must not be empty');
|
|
176
|
+
const orgId = resolveOrgId(auth, program);
|
|
177
|
+
const ep = resolveEndpoint('admin.users.verify_email');
|
|
178
|
+
let data;
|
|
179
|
+
try {
|
|
180
|
+
data = await withStepUp(
|
|
181
|
+
(headers) => client.post(ep.path, { service: ep.service, headers, body: { email, reason, ...(opts.userId ? { user_id: opts.userId } : {}) } }),
|
|
182
|
+
{ client, orgId, scope: 'override', io: ctx.stepUpIO, what: 'Verifying an email address' },
|
|
183
|
+
);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
announceFailure('quox', `admin verify-email ${email}`, err);
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
announce('quox', `admin verify-email ${email}`, { result: `verified · user ${data.user_id}` });
|
|
189
|
+
printSuccess(`Email ${data.email} is now verified. Reason recorded in the audit log.`);
|
|
190
|
+
});
|
|
191
|
+
|
|
157
192
|
// Email / SMTP (QCPRO-SMTP): instance email onboarding parity
|
|
158
193
|
const email = admin
|
|
159
194
|
.command('email')
|
|
@@ -1322,6 +1322,21 @@ export function registerAgentAttachCommands(agent, ctx, program) {
|
|
|
1322
1322
|
if (program.opts().output === 'json') formatDetail(data, { format: 'json' });
|
|
1323
1323
|
});
|
|
1324
1324
|
|
|
1325
|
+
agent
|
|
1326
|
+
.command('attaches')
|
|
1327
|
+
.description('List every live agent attach in the org (agent, splinter, session, lease, heartbeat, expiry)')
|
|
1328
|
+
.action(async () => {
|
|
1329
|
+
auth.requireAuth();
|
|
1330
|
+
const ep = resolveEndpoint('agent-attach.list');
|
|
1331
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
1332
|
+
const rows = Array.isArray(data?.attaches) ? data.attaches : [];
|
|
1333
|
+
if (program.opts().output === 'json') return formatDetail(data, { format: 'json' });
|
|
1334
|
+
if (!rows.length) return printInfo('No live agent attaches in this org.');
|
|
1335
|
+
for (const a of rows) {
|
|
1336
|
+
printInfo(`${a.agent_id} ${a.splinter || 'S1'} · ${a.session_label || '(no label)'} · lease ${a.lease_id} · heartbeat ${fmtAge(a.last_heartbeat_at)} · expires ${a.expires_at || '?'}`);
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
|
|
1325
1340
|
agent
|
|
1326
1341
|
.command('attach-heartbeat')
|
|
1327
1342
|
.description("Heartbeat this session's attach lease, rotating the stored token if one is issued")
|
|
@@ -8,9 +8,23 @@
|
|
|
8
8
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
9
9
|
|
|
10
10
|
import { attachWalkthrough } from './walkthrough.js';
|
|
11
|
+
import { resolveOrgId } from './helpers.js';
|
|
12
|
+
import { confirmInteractive, askTyped } from '../core/confirm.js';
|
|
13
|
+
import { QuoxUserError } from '../core/errors.js';
|
|
14
|
+
import { announce, announceFailure, announceOnFail } from '../core/announce.js';
|
|
15
|
+
import { withStepUp } from '../core/stepUpPrompt.js';
|
|
16
|
+
import { formatOutput, printSuccess } from '../core/output.js';
|
|
17
|
+
import { needsPin } from '../quoxon/approvalsView.js';
|
|
18
|
+
import { describeInboxItem, approvalSourceLabel } from '../quoxon/events.js';
|
|
19
|
+
import { runCallMe, statusLines } from '../core/callMe.js';
|
|
20
|
+
import { isDestructive, destructiveObject } from '../quoxon/destructive.js';
|
|
21
|
+
import { otherOrgsWaiting, crossOrgLines, clientForTarget } from '../core/crossOrgApprovals.js';
|
|
11
22
|
|
|
12
23
|
export function registerApprovalsCommands(program, ctx) {
|
|
13
|
-
const { client, auth } = ctx;
|
|
24
|
+
const { client: baseClient, auth, clientForOrg } = ctx;
|
|
25
|
+
// The client for the org a verb targets: --org another org (a verb in CROSS_ORG, see core/org-override.js) uses that org's own token.
|
|
26
|
+
const clientFor = (orgId) => clientForTarget({ client: baseClient, clientForOrg, sessionOrgId: auth.getOrgId(), orgId });
|
|
27
|
+
const client = baseClient; // `result`, grants and the rest stay on the session org
|
|
14
28
|
|
|
15
29
|
const approvals = program
|
|
16
30
|
.command('approvals')
|
|
@@ -39,6 +53,10 @@ export function registerApprovalsCommands(program, ctx) {
|
|
|
39
53
|
const via = data.decided_via && data.decided_via.channel ? ` via ${data.decided_via.channel}` : '';
|
|
40
54
|
console.log(`decided by ${who} (${data.decided_by.actor_type || 'user'})${via} at ${data.decided_at || 'unknown time'}`);
|
|
41
55
|
}
|
|
56
|
+
if (data.human_decision) {
|
|
57
|
+
const hd = { yes: 'yes (a person approved it with a verified step-up PIN)', no: 'no (not human-verified: denied, withdrawn, expired, approved by an agent, or approved without a verified PIN)', pending: 'pending' }[data.human_decision] || data.human_decision;
|
|
58
|
+
console.log(`human decision: ${hd}`);
|
|
59
|
+
}
|
|
42
60
|
if (data.result === null || data.result === undefined) {
|
|
43
61
|
if (data.status === 'pending') console.log('Still pending: a human has not decided yet.');
|
|
44
62
|
else if (data.status === 'resuming') console.log('Approved, executing. Re-run shortly.');
|
|
@@ -53,6 +71,26 @@ export function registerApprovalsCommands(program, ctx) {
|
|
|
53
71
|
else console.log(JSON.stringify(r, null, 2));
|
|
54
72
|
});
|
|
55
73
|
|
|
74
|
+
// Call-me: the switch for the ApprovalsQ watcher that phones a human when a high-risk
|
|
75
|
+
// approval sits unanswered. It never approves anything; the call only asks the human.
|
|
76
|
+
approvals
|
|
77
|
+
.command('call-me [action]')
|
|
78
|
+
.description('Phone me when a high-risk approval sits unanswered: status (default), on or off (human org admin only)')
|
|
79
|
+
.option('--after <minutes>', 'ring after this many minutes unanswered (1-1440)')
|
|
80
|
+
.option('--quiet <HH:MM-HH:MM>', 'quiet hours, no calls in this window (e.g. 22:00-08:00)')
|
|
81
|
+
.action(announceOnFail('quox', 'approvals call-me', async (action, options) => {
|
|
82
|
+
auth.requireAuth();
|
|
83
|
+
const act = action || 'status';
|
|
84
|
+
if (act === 'status' && (options.after || options.quiet)) {
|
|
85
|
+
throw new QuoxUserError('--after and --quiet change the policy: use `quox approvals call-me on --after 10` (or off).', { code: 'bad_args' });
|
|
86
|
+
}
|
|
87
|
+
const { data, changed } = await runCallMe(client, { action: act, after: options.after, quiet: options.quiet });
|
|
88
|
+
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
89
|
+
const p = (data && data.policy) || {};
|
|
90
|
+
announce('quox', 'approvals call-me', { result: `${changed ? 'set ' : ''}${p.enabled ? `ON after ${p.age_threshold_minutes} min` : 'OFF'}` });
|
|
91
|
+
for (const l of statusLines(data)) console.log(l);
|
|
92
|
+
}));
|
|
93
|
+
|
|
56
94
|
// Fleet standing grants: an org admin pre-approves ONE host + an anchored command
|
|
57
95
|
// shape + a VMID range until an expiry, for ssh_exec. Every use still lands in
|
|
58
96
|
// approvals history with an AEE envelope; anything outside the grant files HITL.
|
|
@@ -61,7 +99,7 @@ export function registerApprovalsCommands(program, ctx) {
|
|
|
61
99
|
.description('Standing grants: org-admin pre-approved fleet command lanes (ssh_exec)');
|
|
62
100
|
|
|
63
101
|
grants
|
|
64
|
-
.command('list')
|
|
102
|
+
.command('list', { isDefault: true })
|
|
65
103
|
.description('List active standing grants (--all includes revoked and expired)')
|
|
66
104
|
.option('--all', 'include revoked and expired grants')
|
|
67
105
|
.action(async (options) => {
|
|
@@ -74,7 +112,7 @@ export function registerApprovalsCommands(program, ctx) {
|
|
|
74
112
|
for (const g of rows) {
|
|
75
113
|
const state = g.revoked_at ? `revoked ${g.revoked_at}` : `expires ${g.expires_at}`;
|
|
76
114
|
const range = g.vmid_min != null ? ` vmids ${g.vmid_min}-${g.vmid_max}` : '';
|
|
77
|
-
console.log(`${g.
|
|
115
|
+
console.log(`${g.tool} on ${g.host}${range} ${g.preset || `${(g.patterns || []).length} patterns`} uses ${g.use_count} ${state} [${g.grant_id}]`);
|
|
78
116
|
console.log(` reason: ${g.reason} (by ${g.created_by})`);
|
|
79
117
|
}
|
|
80
118
|
});
|
|
@@ -107,6 +145,7 @@ export function registerApprovalsCommands(program, ctx) {
|
|
|
107
145
|
const data = await client.post(ep.path, { service: ep.service, body });
|
|
108
146
|
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
109
147
|
const g = data.grant || {};
|
|
148
|
+
announce('approvals', `grant created ${g.tool || 'ssh_exec'}@${g.host || options.host}`, { result: g.grant_id });
|
|
110
149
|
console.log(`Created ${g.grant_id}: ${g.tool}@${g.host}${g.vmid_min != null ? ` vmids ${g.vmid_min}-${g.vmid_max}` : ''}, expires ${g.expires_at}`);
|
|
111
150
|
console.log(`Revoke any time: quox approvals grants revoke ${g.grant_id}`);
|
|
112
151
|
});
|
|
@@ -119,6 +158,157 @@ export function registerApprovalsCommands(program, ctx) {
|
|
|
119
158
|
const ep = resolveEndpoint('v1.approvals.grants.revoke', { id: grantId });
|
|
120
159
|
const data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
121
160
|
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
161
|
+
announce('approvals', `grant revoked ${grantId}`);
|
|
122
162
|
console.log(`Revoked ${data.grant?.grant_id || grantId} at ${data.grant?.revoked_at || 'now'}.`);
|
|
123
163
|
});
|
|
164
|
+
|
|
165
|
+
// ---- list | approve | deny: the verb people expect, over the SAME server path as `quox inbox respond`.
|
|
166
|
+
const OPEN_QUERY = { status: 'open', kind: 'approval', limit: 200 };
|
|
167
|
+
const listOpen = async (orgId, status, cl = client) => {
|
|
168
|
+
const ep = resolveEndpoint('inbox.list', { orgId });
|
|
169
|
+
const data = await cl.get(ep.path, { service: ep.service, query: { ...OPEN_QUERY, ...(status ? { status } : {}) } });
|
|
170
|
+
const items = (data && (data.items || data)) || [];
|
|
171
|
+
return Array.isArray(items) ? items : [];
|
|
172
|
+
};
|
|
173
|
+
const shortId = (id) => String(id).slice(-6);
|
|
174
|
+
|
|
175
|
+
async function pickApproval(orgId, given, cl = client) {
|
|
176
|
+
const want = String(given).trim().toUpperCase();
|
|
177
|
+
const items = await listOpen(orgId, undefined, cl);
|
|
178
|
+
const hits = items.filter((it) => {
|
|
179
|
+
const id = String(it.id).toUpperCase();
|
|
180
|
+
return id === want || id.endsWith(want) || String(it.aee_correlation_id || '').toUpperCase() === want;
|
|
181
|
+
});
|
|
182
|
+
const exact = hits.filter((it) => String(it.id).toUpperCase() === want);
|
|
183
|
+
if (exact.length === 1) return exact[0];
|
|
184
|
+
if (hits.length === 1) return hits[0];
|
|
185
|
+
if (hits.length > 1) {
|
|
186
|
+
throw new QuoxUserError(`"${given}" is ambiguous: it matches ${hits.length} open approvals (${hits.map((h) => shortId(h.id)).join(', ')}).`, {
|
|
187
|
+
next: 'use more of the id, or the full id from `quox approvals list`.', exitCode: 2, code: 'ambiguous_id',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
throw new QuoxUserError(`No open approval matches "${given}" in this org.`, {
|
|
191
|
+
next: '`quox approvals list` shows what is waiting; `quox inbox get <id>` finds an answered one.', exitCode: 5, code: 'not_found',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function summaryLine(it) {
|
|
196
|
+
const d = describeInboxItem(it);
|
|
197
|
+
const what = [d.tool, d.target && d.target !== d.tool ? d.target : null].filter(Boolean).join(' on ') || d.title || it.title || it.id;
|
|
198
|
+
const who = d.by || approvalSourceLabel(it);
|
|
199
|
+
return `[${shortId(it.id)}] ${what} (asked by ${who})`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function decide(verb, given, options) {
|
|
203
|
+
auth.requireAuth();
|
|
204
|
+
const orgId = resolveOrgId(auth, program);
|
|
205
|
+
const client = await clientFor(orgId); // another org only when --org names one
|
|
206
|
+
const it = await pickApproval(orgId, given, client);
|
|
207
|
+
const approve = verb === 'approve';
|
|
208
|
+
console.log(`${approve ? 'Approve' : 'Deny'}: ${summaryLine(it)}`);
|
|
209
|
+
if (approve) {
|
|
210
|
+
if (needsPin(it)) {
|
|
211
|
+
throw new QuoxUserError('High risk: a yes needs your step-up PIN, which a plain command cannot take.', {
|
|
212
|
+
next: 'answer it in `quox` with /approvals, or deny it here with `quox approvals deny`.', exitCode: 4, code: 'step_up_required',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
if (isDestructive(it)) {
|
|
216
|
+
const { name } = destructiveObject(it);
|
|
217
|
+
if (!process.stdin.isTTY) {
|
|
218
|
+
throw new QuoxUserError(`This deletes "${name}": its name must be typed at a terminal. --yes does not cover it.`, {
|
|
219
|
+
next: 'run this command in an interactive terminal.', exitCode: 2, code: 'no_terminal',
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const typed = await askTyped(`This is destructive. Type "${name}" to approve`);
|
|
223
|
+
if (String(typed ?? '').trim() !== name) {
|
|
224
|
+
throw new QuoxUserError(`What you typed did not match "${name}". Nothing was approved.`, { exitCode: 1, code: 'confirm_mismatch' });
|
|
225
|
+
}
|
|
226
|
+
} else if (!options.yes) {
|
|
227
|
+
const ok = await confirmInteractive('Approve this?', { flag: '--yes', purpose: 'approve' });
|
|
228
|
+
if (!ok) { console.log('Not approved. The request stays open.'); return; }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
const ep = resolveEndpoint('inbox.respond', { orgId, itemId: it.id });
|
|
232
|
+
try {
|
|
233
|
+
const body = { action: approve ? 'approve' : 'reject', ...(options.note ? { comment: options.note } : {}) };
|
|
234
|
+
// DS-010 / #1335: only an approve can hit a step-up sink; ask for the PIN (hidden, TTY only) and retry once.
|
|
235
|
+
if (approve) await withStepUp((headers) => client.post(ep.path, { service: ep.service, headers, body }), { client, orgId, io: ctx.stepUpIO, what: 'This approval' });
|
|
236
|
+
else await client.post(ep.path, { service: ep.service, body });
|
|
237
|
+
} catch (err) {
|
|
238
|
+
announceFailure('approvals', `${approve ? 'approve' : 'deny'} ${shortId(it.id)}`, err);
|
|
239
|
+
if (err && (err.status === 403 || err.status === 401) && !(err instanceof QuoxUserError)) {
|
|
240
|
+
const msg = err.body?.message || err.body?.error || err.message;
|
|
241
|
+
const denied = new QuoxUserError(String(msg), { exitCode: 4, code: err.body?.error || 'forbidden', status: err.status, body: err.body });
|
|
242
|
+
Object.defineProperty(denied, 'quoxAnnounced', { value: true });
|
|
243
|
+
throw denied;
|
|
244
|
+
}
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
announce('approvals', `${approve ? 'approved' : 'denied'} ${shortId(it.id)}`, { result: summaryLine(it).replace(/^\[\w+\]\s*/, '').replace(/\s+\(asked by .*$/, '') });
|
|
248
|
+
printSuccess(`${approve ? 'Approved' : 'Denied'} ${shortId(it.id)}.`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
approvals
|
|
252
|
+
.command('list')
|
|
253
|
+
.description('List approvals waiting for a human (same call as `quox inbox list`, approvals only)')
|
|
254
|
+
.option('--status <status>', 'open (default), all, or a status list')
|
|
255
|
+
.option('--limit <n>', 'max results', '50')
|
|
256
|
+
.action(async (options) => {
|
|
257
|
+
auth.requireAuth();
|
|
258
|
+
const orgId = resolveOrgId(auth, program);
|
|
259
|
+
const cl = await clientFor(orgId);
|
|
260
|
+
const ep = resolveEndpoint('inbox.list', { orgId });
|
|
261
|
+
const data = await cl.get(ep.path, { service: ep.service, query: { ...OPEN_QUERY, limit: options.limit, ...(options.status ? { status: options.status } : {}) } });
|
|
262
|
+
const items = ((data && (data.items || data)) || []).map((it) => ({ ...it, short: shortId(it.id), what: summaryLine(it).replace(/^\[\w+\]\s*/, '') }));
|
|
263
|
+
formatOutput({
|
|
264
|
+
columns: ['short', 'what', 'status', 'created_at', 'id'],
|
|
265
|
+
headers: ['ID', 'What', 'Status', 'Created', 'Full id'],
|
|
266
|
+
rows: items,
|
|
267
|
+
}, { format: program.opts().output, emptyMessage: 'No approvals are waiting.' });
|
|
268
|
+
// Waiting in the user's OTHER orgs: a count and the exact command, table output only (json stays pure data).
|
|
269
|
+
if (program.opts().output === 'table' || !program.opts().output) {
|
|
270
|
+
for (const l of crossOrgLines(await otherOrgsWaiting({ client: baseClient, currentOrgId: orgId }))) console.log(l);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
approvals
|
|
275
|
+
.command('approve <id>')
|
|
276
|
+
.description('Approve one waiting request by id (full id or the short tail on its card). A high-risk yes needs a PIN, so use `quox`.')
|
|
277
|
+
.option('--note <text>', 'a note recorded with the decision')
|
|
278
|
+
.option('-y, --yes', 'skip the y/N question (not a destructive delete, which needs its name typed)')
|
|
279
|
+
.addHelpText('after', '\nExamples:\n quox approvals list\n quox approvals approve D05HQY\n quox approvals approve D05HQY --yes --note "checked the host"\n')
|
|
280
|
+
.action(announceOnFail('approvals', (id) => `approve ${id}`, (id, options) => decide('approve', id, options)));
|
|
281
|
+
|
|
282
|
+
approvals
|
|
283
|
+
.command('deny <id>')
|
|
284
|
+
.description('Deny one waiting request by id (full id or the short tail on its card)')
|
|
285
|
+
.option('--note <text>', 'why, recorded with the decision')
|
|
286
|
+
.addHelpText('after', '\nExamples:\n quox approvals deny FJ340S --note "wrong host"\n')
|
|
287
|
+
.action(announceOnFail('approvals', (id) => `deny ${id}`, (id, options) => decide('deny', id, options)));
|
|
288
|
+
|
|
289
|
+
approvals
|
|
290
|
+
.command('withdraw <id>')
|
|
291
|
+
.description('Take back an approval request YOU raised (full id). It is marked withdrawn, never denied; the held call cannot run; only the identity that raised it may do this')
|
|
292
|
+
.option('--note <text>', 'why, recorded with the withdrawal')
|
|
293
|
+
.addHelpText('after', '\nExamples:\n quox approvals withdraw 01KXYZ... --note "raised by mistake"\n')
|
|
294
|
+
.action(announceOnFail('approvals', (id) => `withdraw ${id}`, async (given, options) => {
|
|
295
|
+
auth.requireAuth();
|
|
296
|
+
const orgId = resolveOrgId(auth, program);
|
|
297
|
+
// The full item id or approval id goes straight to the server, which checks you are the requester. A short tail is
|
|
298
|
+
// looked up among the open approvals you can see; one you cannot see is not yours to withdraw by tail.
|
|
299
|
+
let id = String(given).trim();
|
|
300
|
+
if (id.length < 20) { const it = await pickApproval(orgId, id); id = it.id; }
|
|
301
|
+
const ep = resolveEndpoint('inbox.withdraw', { orgId, itemId: id });
|
|
302
|
+
try {
|
|
303
|
+
await client.post(ep.path, { service: ep.service, body: { ...(options.note ? { reason: options.note } : {}) } });
|
|
304
|
+
} catch (err) {
|
|
305
|
+
if (err && (err.status === 403 || err.status === 404 || err.status === 400) && !(err instanceof QuoxUserError)) {
|
|
306
|
+
const msg = err.body?.message || err.body?.error || err.message;
|
|
307
|
+
throw new QuoxUserError(String(msg), { exitCode: err.status === 403 ? 4 : 5, code: err.body?.error || 'withdraw_refused', status: err.status, body: err.body });
|
|
308
|
+
}
|
|
309
|
+
throw err;
|
|
310
|
+
}
|
|
311
|
+
announce('approvals', `withdrew ${shortId(id)}`, { result: 'withdrawn, nothing ran' });
|
|
312
|
+
printSuccess(`Withdrew ${shortId(id)}. It is marked withdrawn and cannot run.`);
|
|
313
|
+
}));
|
|
124
314
|
}
|
package/lib/commands/ask.js
CHANGED
|
@@ -130,7 +130,7 @@ export function registerAskCommands(program, { client, auth }) {
|
|
|
130
130
|
const d = (await client.get(ep.path, { service: ep.service })).decision;
|
|
131
131
|
if (program.opts().output === 'json') { console.log(JSON.stringify(d, null, 2)); return; }
|
|
132
132
|
console.log(`${d.state}${d.answer?.option ? ` ${d.answer.option}` : ''}${d.answer?.note ? ` (${d.answer.note})` : ''}`);
|
|
133
|
-
if (d.state === 'expired' || d.state === 'denied') process.exitCode = EXIT_NO_ANSWER;
|
|
133
|
+
if (d.state === 'expired' || d.state === 'denied' || d.state === 'withdrawn') process.exitCode = EXIT_NO_ANSWER;
|
|
134
134
|
});
|
|
135
135
|
|
|
136
136
|
ask
|
package/lib/commands/auth.js
CHANGED
|
@@ -7,7 +7,19 @@ import { formatDetail, printSuccess, printInfo } from '../core/output.js';
|
|
|
7
7
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
8
8
|
import { loadResidentProfile, residentLogin, resolveResidentBaseUrl } from '../core/residentAuth.js';
|
|
9
9
|
import { readStoredKit, resolveMyAttach, sessionKey, myAttachState } from './agentAttach.js';
|
|
10
|
+
import { loginPrefillEmail, profileHoldingEmail, isNonDefaultProfile, sameIdentityWarning } from '../core/loginGuard.js';
|
|
10
11
|
import { resolveActing } from '../core/actingAs.js';
|
|
12
|
+
import { QuoxUserError } from '../core/errors.js';
|
|
13
|
+
|
|
14
|
+
const PASSWORD_POLICY_HINT = 'Password policy: at least 12 characters, with an upper-case letter, a lower-case letter, a digit and a symbol.';
|
|
15
|
+
|
|
16
|
+
/** whoami line for "-p/--profile given": say which agent is acting when the profile is an agent identity. */
|
|
17
|
+
export function profileActingLine({ actorType, profile, user }) {
|
|
18
|
+
if (actorType === 'agent') {
|
|
19
|
+
return ` Acting as agent ${user || 'unknown'} via profile ${profile || 'unknown'} (-p/--profile given)`;
|
|
20
|
+
}
|
|
21
|
+
return ' Acting as yourself (-p/--profile given)';
|
|
22
|
+
}
|
|
11
23
|
|
|
12
24
|
export function registerAuthCommands(program, ctx) {
|
|
13
25
|
const { config, auth, client } = ctx;
|
|
@@ -122,6 +134,7 @@ export function registerAuthCommands(program, ctx) {
|
|
|
122
134
|
type: 'input',
|
|
123
135
|
name: 'email',
|
|
124
136
|
message: 'Email:',
|
|
137
|
+
default: loginPrefillEmail(config) || undefined,
|
|
125
138
|
validate: v => v.includes('@') || 'Please enter a valid email',
|
|
126
139
|
}]);
|
|
127
140
|
email = ans.email;
|
|
@@ -138,6 +151,21 @@ export function registerAuthCommands(program, ctx) {
|
|
|
138
151
|
}
|
|
139
152
|
}
|
|
140
153
|
|
|
154
|
+
// Stop agent-only profiles quietly becoming another profile's (the owner's) identity.
|
|
155
|
+
if (isNonDefaultProfile(config)) {
|
|
156
|
+
const holder = profileHoldingEmail(config, email, config.getActiveProfile?.()?.url || null);
|
|
157
|
+
if (holder) {
|
|
158
|
+
const msg = sameIdentityWarning(email, holder, config.getActiveProfileName());
|
|
159
|
+
if (!process.stdin.isTTY) {
|
|
160
|
+
console.error(`Error: ${msg}\nRefusing in a non-interactive shell. Use the profile's own account, or run interactively and answer y.`);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
const { default: inquirer } = await import('inquirer');
|
|
164
|
+
const ans = await inquirer.prompt([{ type: 'confirm', name: 'ok', message: msg.replace(/ y\/N$/, ''), default: false }]);
|
|
165
|
+
if (!ans.ok) { printInfo('Cancelled. Nothing was sent.'); return; }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
141
169
|
const baseUrl = config.getServiceUrl('auth');
|
|
142
170
|
const ora = (await import('ora')).default;
|
|
143
171
|
const spinner = ora('Authenticating...').start();
|
|
@@ -196,6 +224,7 @@ export function registerAuthCommands(program, ctx) {
|
|
|
196
224
|
program
|
|
197
225
|
.command('whoami')
|
|
198
226
|
.description('Show current user and session info')
|
|
227
|
+
.addHelpText('after', '\nAdd --json for machine output (user, email, org, role, mfa, session, profile, actor_type, host_id).')
|
|
199
228
|
.action(async () => {
|
|
200
229
|
auth.requireAuth();
|
|
201
230
|
const data = await client.get(resolveEndpoint('auth.me.list').path, { service: resolveEndpoint('auth.me.list').service });
|
|
@@ -309,7 +338,7 @@ export function registerAuthCommands(program, ctx) {
|
|
|
309
338
|
} else if (acting.source === 'self') {
|
|
310
339
|
printInfo(' Acting as yourself (--as self)');
|
|
311
340
|
} else if (acting.source === 'profile') {
|
|
312
|
-
printInfo(
|
|
341
|
+
printInfo(profileActingLine({ actorType, profile: info.profile, user: info.user }));
|
|
313
342
|
} else if (myAttachState().state === 'ambiguous') {
|
|
314
343
|
printInfo(' Acting as yourself: this session has more than one attach (ambiguous), so no agent default applies. Pick one with --as <agent>.');
|
|
315
344
|
}
|
|
@@ -337,18 +366,42 @@ export function registerAuthCommands(program, ctx) {
|
|
|
337
366
|
.command('signup')
|
|
338
367
|
.description('Create a new user account')
|
|
339
368
|
.requiredOption('--email <email>', 'email address')
|
|
340
|
-
.
|
|
369
|
+
.option('--password <password>', 'password (prefer --password-stdin: argv is visible in process lists)')
|
|
370
|
+
.option('--password-stdin', 'read the password from stdin (one line), no secret in argv or shell history')
|
|
341
371
|
.requiredOption('--name <name>', 'display name')
|
|
342
372
|
.action(async (options) => {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
373
|
+
let password = options.password;
|
|
374
|
+
if (!password && options.passwordStdin) {
|
|
375
|
+
const chunks = [];
|
|
376
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
377
|
+
password = Buffer.concat(chunks).toString('utf8').split('\n')[0].trim();
|
|
378
|
+
if (!password) throw new QuoxUserError('--password-stdin given but stdin was empty', { next: 'Pipe the password in, for example: printf %s "$PW" | quox signup --password-stdin ...' });
|
|
379
|
+
} else if (password) {
|
|
380
|
+
console.error('Warning: argv is visible to other users (process list) and lands in shell history, so a password given with --password is exposed. Prefer --password-stdin.');
|
|
381
|
+
}
|
|
382
|
+
if (!password) {
|
|
383
|
+
throw new QuoxUserError('A password is required. Use --password-stdin (reads one line from stdin) or --password <password>.');
|
|
384
|
+
}
|
|
385
|
+
const ep = resolveEndpoint('auth.signup');
|
|
386
|
+
try {
|
|
387
|
+
await client.post(ep.path, {
|
|
388
|
+
service: ep.service,
|
|
389
|
+
body: { email: options.email, password, display_name: options.name },
|
|
390
|
+
noAuth: true,
|
|
391
|
+
});
|
|
392
|
+
} catch (err) {
|
|
393
|
+
if (err?.status === 400 && err?.code === 'validation_error') {
|
|
394
|
+
const reason = err.body?.message || err.message;
|
|
395
|
+
const list = Array.isArray(err.body?.errors) ? err.body.errors.filter((e) => typeof e === 'string' && e !== reason) : [];
|
|
396
|
+
const detail = [reason, ...list].join('; ');
|
|
397
|
+
const aboutPassword = /password/i.test(detail);
|
|
398
|
+
throw new QuoxUserError(`Signup was rejected: ${detail}`, {
|
|
399
|
+
next: aboutPassword ? PASSWORD_POLICY_HINT : null,
|
|
400
|
+
code: 'validation_error', status: 400, body: err.body,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
352
405
|
printSuccess(`Account created for ${options.email}. Check your email for verification.`);
|
|
353
406
|
});
|
|
354
407
|
|
package/lib/commands/backup.js
CHANGED
|
@@ -37,12 +37,19 @@ export function registerBackupCommands(program, ctx) {
|
|
|
37
37
|
auth.requireAuth();
|
|
38
38
|
const orgId = resolveOrgId(auth, program);
|
|
39
39
|
const data = await client.get(resolveEndpoint('backups.list', { orgId }).path, { service: resolveEndpoint('backups.list', { orgId }).service, query: { limit: options.limit } });
|
|
40
|
+
const items = data.items || data.backups || data;
|
|
41
|
+
// Name leads (ids are for scripting: --json, -q). A failed row says why, clipped.
|
|
42
|
+
const clip = (t, n = 60) => { const x = String(t).replace(/\s+/g, ' ').trim(); return x.length > n ? `${x.slice(0, n - 1)}…` : x; };
|
|
43
|
+
const rows = (Array.isArray(items) ? items : []).map((b) => ({
|
|
44
|
+
...b,
|
|
45
|
+
status: b.status === 'failed' && b.error_message ? `failed: ${clip(b.error_message)}` : b.status,
|
|
46
|
+
}));
|
|
40
47
|
formatOutput({
|
|
41
48
|
// fleet #392: backups has name/backup_type/archive_size_bytes.
|
|
42
|
-
columns: ['
|
|
43
|
-
headers: ['
|
|
44
|
-
rows
|
|
45
|
-
}, { format: program.opts().output });
|
|
49
|
+
columns: ['name', 'backup_type', 'archive_size_bytes', 'status', 'created_at', 'id'],
|
|
50
|
+
headers: ['Name', 'Type', 'Size (bytes)', 'Status', 'Created', 'ID'],
|
|
51
|
+
rows,
|
|
52
|
+
}, { format: program.opts().output, raw: Array.isArray(items) ? items : undefined, quietColumn: 'id' });
|
|
46
53
|
});
|
|
47
54
|
|
|
48
55
|
backup
|