quoxcli 0.3.1 → 0.5.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 +61 -0
- package/README.md +2 -2
- package/bin/quox.js +19 -1
- package/bin/quoxon.js +6 -1
- package/lib/api/endpoints/actions.js +15 -0
- package/lib/api/endpoints/admin.js +27 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/blog.js +16 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/contactsq.js +4 -0
- package/lib/api/endpoints/devServerRegistry.js +25 -0
- package/lib/api/endpoints/engagement.js +26 -0
- package/lib/api/endpoints/evidence.js +20 -0
- package/lib/api/endpoints/hat.js +14 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +19 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/loops.js +26 -0
- package/lib/api/endpoints/mon.js +295 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/server.js +21 -0
- package/lib/api/endpoints/setup.js +6 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +47 -0
- package/lib/commands/actions.js +81 -11
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agent.js +11 -4
- package/lib/commands/agentAttach.js +61 -7
- package/lib/commands/approvals.js +198 -3
- package/lib/commands/ask.js +4 -2
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/blog.js +110 -0
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +142 -28
- package/lib/commands/chatCompanion.js +27 -0
- package/lib/commands/chatState.js +45 -0
- package/lib/commands/claude.js +7 -33
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/contacts.js +85 -4
- package/lib/commands/contactsSendAt.js +21 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/engagement.js +90 -0
- package/lib/commands/evidence.js +155 -0
- package/lib/commands/ext.js +23 -0
- package/lib/commands/fleet.js +25 -0
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/hat.js +76 -0
- package/lib/commands/image.js +68 -0
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +1123 -0
- package/lib/commands/kumaSla.js +174 -0
- package/lib/commands/licence.js +39 -1
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +318 -0
- package/lib/commands/lucid.js +1 -0
- package/lib/commands/mail.js +161 -20
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +55 -144
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +259 -0
- package/lib/commands/objective.js +53 -3
- package/lib/commands/offload.js +2 -3
- package/lib/commands/org.js +21 -4
- package/lib/commands/plan.js +17 -0
- package/lib/commands/provider.js +87 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/quoxseo.js +31 -0
- package/lib/commands/research.js +79 -0
- package/lib/commands/rows.js +9 -0
- package/lib/commands/run.js +20 -2
- package/lib/commands/scope.js +2 -2
- package/lib/commands/server.js +150 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/shortcut.js +41 -0
- package/lib/commands/skills.js +24 -5
- 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/vm.js +2 -2
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +824 -0
- package/lib/commands/wazuhIntake.js +317 -0
- package/lib/commands/wazuhReport.js +192 -0
- package/lib/commands/workflow.js +47 -4
- package/lib/commands/x.js +50 -0
- package/lib/commands/youtube.js +172 -0
- package/lib/core/actionItems.js +115 -0
- package/lib/core/agentAlias.js +2 -0
- package/lib/core/agentFamilies.js +7 -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 +37 -3
- package/lib/core/commandRegistrars.js +17 -0
- package/lib/core/config.js +29 -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/findingReconcile.js +90 -0
- package/lib/core/global-flags.js +1 -1
- package/lib/core/governedSession.js +136 -8
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/liveSessionsReport.js +1 -1
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/loopGlyph.js +97 -0
- package/lib/core/offload.js +73 -7
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +8 -8
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/sessionHeartbeat.js +12 -8
- package/lib/core/shortcut.js +83 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +5 -0
- package/lib/core/walkthrough.js +9 -5
- package/lib/dragon/companionView.js +34 -0
- package/lib/dragon/frames.js +118 -0
- package/lib/dragon/jobs.js +123 -0
- package/lib/dragon/renderWorker.js +15 -0
- package/lib/dragon/select.js +72 -0
- package/lib/evidence/bundleExport.js +47 -0
- package/lib/evidence/bundleVerify.js +77 -0
- package/lib/quoxon/EXTENSIONS.md +146 -0
- package/lib/quoxon/api.js +364 -27
- package/lib/quoxon/app.js +1629 -183
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +64 -8
- package/lib/quoxon/approvalSentence.js +48 -0
- package/lib/quoxon/approvalsView.js +124 -7
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +1 -1
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +22 -3
- package/lib/quoxon/commands.js +110 -15
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/countdownLine.js +10 -1
- package/lib/quoxon/deck.js +30 -0
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/demo.autoplay.js +1 -1
- package/lib/quoxon/demo.js +3 -1
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dividerAocl.js +4 -3
- package/lib/quoxon/dividerRule.js +3 -3
- package/lib/quoxon/dnd.js +71 -0
- package/lib/quoxon/dragonCompanion.js +285 -0
- package/lib/quoxon/dragonModes.js +1 -0
- package/lib/quoxon/dragonMoments.js +160 -0
- package/lib/quoxon/dragonReads.js +68 -0
- package/lib/quoxon/dragonView.js +2 -2
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +67 -10
- package/lib/quoxon/extensions.js +636 -0
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +182 -0
- package/lib/quoxon/installCli.js +79 -0
- package/lib/quoxon/keys.js +4 -4
- package/lib/quoxon/loopCard.js +460 -0
- package/lib/quoxon/loopCards.js +189 -0
- package/lib/quoxon/loopStream.js +98 -0
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +29 -5
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/mouse.js +12 -1
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objectiveOffer.js +82 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/objects/index.js +22 -4
- package/lib/quoxon/objects/model.js +33 -0
- package/lib/quoxon/objects/proof.js +12 -4
- package/lib/quoxon/onboarding/connectServer.js +162 -0
- package/lib/quoxon/onboarding/copy.js +6 -0
- package/lib/quoxon/onboarding/index.js +5 -43
- package/lib/quoxon/onboarding/probe.js +20 -1
- package/lib/quoxon/onboarding/shortcutOffer.js +28 -0
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +49 -1
- package/lib/quoxon/planSteps.js +51 -0
- package/lib/quoxon/print.js +8 -3
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/qdragonFly.js +7 -6
- package/lib/quoxon/qmotion.js +17 -0
- package/lib/quoxon/render.js +187 -41
- package/lib/quoxon/roster.js +36 -17
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/rows.js +244 -1
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +86 -0
- package/lib/quoxon/serverChip.js +42 -0
- package/lib/quoxon/session.js +10 -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 +18 -5
- package/lib/quoxon/slash/broadcastRun.js +13 -2
- package/lib/quoxon/slash/cliCommand.js +42 -1
- package/lib/quoxon/slash/cliLeaves.json +139 -2
- package/lib/quoxon/slash/evidenceCmd.js +34 -0
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/palette.js +23 -2
- package/lib/quoxon/slash/paletteController.js +10 -4
- package/lib/quoxon/slash/qpeCmd.js +35 -0
- package/lib/quoxon/slash/restart.js +31 -4
- package/lib/quoxon/slash/resume.js +18 -1
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slash/todoCmd.js +122 -0
- package/lib/quoxon/slashSpinner.js +35 -0
- package/lib/quoxon/slotAttach.js +8 -0
- package/lib/quoxon/slotChat.js +17 -0
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/suggestCommand.js +34 -3
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/toolCards.js +30 -0
- package/lib/quoxon/transcript.js +2 -1
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +81 -0
- package/lib/quoxon/windowOrg.js +11 -1
- package/lib/server/devServers.js +137 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +55 -0
- package/package.json +4 -2
- package/vendor/dragonlab/SOURCE +4 -0
- package/vendor/dragonlab/design/cipher/v1/cipher-master.svg +46 -0
- package/vendor/dragonlab/design/cipher/v2/character.mjs +49 -0
- package/vendor/dragonlab/design/cipher/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/cipher/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/drago/v1/character.mjs +19 -0
- package/vendor/dragonlab/design/drago/v1/drago-master.svg +56 -0
- package/vendor/dragonlab/design/drago/v2/character.mjs +87 -0
- package/vendor/dragonlab/design/drago/v4/character.mjs +67 -0
- package/vendor/dragonlab/design/drago/v4/profile.mjs +35 -0
- package/vendor/dragonlab/design/drago/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/drago/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/hyperion/v1/hyperion-master.svg +56 -0
- package/vendor/dragonlab/design/hyperion/v2/character.mjs +79 -0
- package/vendor/dragonlab/design/hyperion/v2/review.mjs +33 -0
- package/vendor/dragonlab/design/hyperion/v2/watch.mjs +56 -0
- package/vendor/dragonlab/design/lucid/recovery/arrival.mjs +22 -0
- package/vendor/dragonlab/design/lucid/recovery/character.mjs +106 -0
- package/vendor/dragonlab/design/lucid/recovery/layout.mjs +50 -0
- package/vendor/dragonlab/design/lucid/recovery/lucid-master.svg +78 -0
- package/vendor/dragonlab/design/lucid/recovery/motion.mjs +42 -0
- package/vendor/dragonlab/design/lucid/recovery/render.mjs +121 -0
- package/vendor/dragonlab/design/lucid/recovery/review.mjs +68 -0
- package/vendor/dragonlab/design/lucid/recovery/verify.mjs +84 -0
- package/vendor/dragonlab/design/lucid/recovery/watch.mjs +56 -0
- 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/seer/v1/seer-master.svg +53 -0
- package/vendor/dragonlab/design/seer/v2/character.mjs +88 -0
- package/vendor/dragonlab/design/seer/v3/character.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/character.mjs +66 -0
- package/vendor/dragonlab/design/seer/v4/review.mjs +33 -0
- package/vendor/dragonlab/design/seer/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/stage3/companion-review.mjs +28 -0
- package/vendor/dragonlab/design/stage3/sentinel-art.js +39 -0
- package/vendor/dragonlab/design/stage3/sentinel-braille.js +216 -0
- package/vendor/dragonlab/design/stage3/sentinel-character.js +257 -0
- package/vendor/dragonlab/design/stage3/sentinel-icon.js +103 -0
- package/vendor/dragonlab/design/stage3/sentinel-warden-eyes.js +95 -0
- package/vendor/dragonlab/design/stage3/svg-coverage.js +148 -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/factory/svg-rig.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +108 -0
- package/vendor/dragonlab/integration/companions-v1/cipher.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/companion.mjs +63 -0
- package/vendor/dragonlab/integration/companions-v1/drago.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/hyperion.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/lucid.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/seer.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/sentinel.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +28 -0
- package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
- package/vendor/dragonlab/integration/hyperion-v3/activity.mjs +76 -0
- package/vendor/dragonlab/lib/power-effects.js +294 -0
- package/vendor/dragonlab/lib/power-registry.js +74 -0
- package/vendor/dragonlab/vendor/quox-dragon/qart.js +348 -0
- package/vendor/dragonlab/vendor/quox-dragon/qeyes.js +213 -0
package/lib/commands/qlarity.js
CHANGED
|
@@ -49,6 +49,25 @@ function annotate(err) {
|
|
|
49
49
|
throw err;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Inspectors whose server-side run() calls assertSelector (qlarity/src/inspectors.js).
|
|
54
|
+
* The registry lives in another repo and `inspectors list` does not expose the
|
|
55
|
+
* flag, so this mirrors it; storage, dom_summary, layout_health and
|
|
56
|
+
* responsive_sweep default or skip the selector and are deliberately absent.
|
|
57
|
+
*/
|
|
58
|
+
export const SELECTOR_INSPECTORS = ['computed_style', 'pseudo_style', 'text_content', 'attribute', 'count', 'bounding_box', 'expect'];
|
|
59
|
+
|
|
60
|
+
/** Keep scheme+host+port only (lowercased); anything unparseable goes through for the server to explain. */
|
|
61
|
+
export function normaliseOrigin(input) {
|
|
62
|
+
try {
|
|
63
|
+
const u = new URL(String(input).trim());
|
|
64
|
+
if (!/^https?:$/.test(u.protocol)) return input;
|
|
65
|
+
return u.origin;
|
|
66
|
+
} catch {
|
|
67
|
+
return input;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
52
71
|
// quox#907: a browser login is slower than a read; well above the client's 30s default.
|
|
53
72
|
const LOGIN_TIMEOUT_MS = 120000;
|
|
54
73
|
|
|
@@ -308,14 +327,21 @@ export function registerQlarityCommands(program, ctx) {
|
|
|
308
327
|
al.command('add <origin>')
|
|
309
328
|
.description('Declare an origin this org may inspect, e.g. https://staging.example.com')
|
|
310
329
|
.option('--note <text>', 'why it is allowed, shown alongside it later')
|
|
311
|
-
.action(async (
|
|
330
|
+
.action(async (rawOrigin, opts) => {
|
|
331
|
+
const origin = normaliseOrigin(rawOrigin);
|
|
312
332
|
try {
|
|
333
|
+
if (origin !== rawOrigin) process.stdout.write(`allowlisting ${origin} (scheme+host+port of ${rawOrigin})\n`);
|
|
313
334
|
const d = await call(client, 'qlarity.orgAllowlist.add', {}, { origin, note: opts.note });
|
|
314
335
|
process.stdout.write(`\u{1F50D} QLARITY: added ${d.entry.origin}\n`);
|
|
315
336
|
process.stdout.write(` ${d.origins.length} origin(s) now declared for this org\n`);
|
|
316
337
|
} catch (err) {
|
|
317
338
|
// A refusal here is usually a wildcard or a typo, and the server sends
|
|
318
339
|
// the reason. Surface it instead of a bare 400.
|
|
340
|
+
if (err?.status === 403) {
|
|
341
|
+
process.stderr.write(`Not added: adding an origin is an org-admin action by design (an agent cannot self-authorize egress).\nRetry as an org admin: quox -p <admin-profile> qlarity origins add ${origin}\n`);
|
|
342
|
+
process.exitCode = 4;
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
319
345
|
if (err?.status === 400 && err?.message) {
|
|
320
346
|
process.stderr.write(`Not added: ${err.message}\n`);
|
|
321
347
|
process.exitCode = 1;
|
|
@@ -395,6 +421,11 @@ export function registerQlarityCommands(program, ctx) {
|
|
|
395
421
|
.option('--session <id>', 'reuse an existing session (e.g. one left open by `quox qlarity login`) instead of opening a fresh one')
|
|
396
422
|
.option('--json', 'raw JSON output')
|
|
397
423
|
.action(async (url, options) => {
|
|
424
|
+
if (SELECTOR_INSPECTORS.includes(options.inspector) && !options.selector) {
|
|
425
|
+
process.stderr.write(`${options.inspector} needs --selector. Try: quox qlarity read ${url} --inspector ${options.inspector} --selector 'body'\n`);
|
|
426
|
+
process.exitCode = 2;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
398
429
|
let sessionId = null;
|
|
399
430
|
let reused = false;
|
|
400
431
|
try {
|
package/lib/commands/quoxseo.js
CHANGED
|
@@ -231,6 +231,37 @@ export function registerQuoxseoCommands(program, ctx) {
|
|
|
231
231
|
} catch (err) { annotate(err); }
|
|
232
232
|
});
|
|
233
233
|
|
|
234
|
+
s.command('queries')
|
|
235
|
+
.description('Ranked Search Console query rows: query, page, impressions, clicks, CTR, position')
|
|
236
|
+
.option('--sort <s>', 'impressions | clicks | position-opportunity (high impressions at position 5-20)', 'impressions')
|
|
237
|
+
.option('--limit <n>', 'max rows (default 25, max 100)', '25')
|
|
238
|
+
.option('--filter <text>', 'only queries containing this text, e.g. "conversation"')
|
|
239
|
+
.option('--gaps', 'topic-gap view: queries with impressions that no crawled page targets')
|
|
240
|
+
.action(async (opts) => {
|
|
241
|
+
try {
|
|
242
|
+
const qs = new URLSearchParams();
|
|
243
|
+
qs.set('sort', opts.sort);
|
|
244
|
+
qs.set('limit', opts.limit);
|
|
245
|
+
if (opts.filter) qs.set('filter', opts.filter);
|
|
246
|
+
if (opts.gaps) qs.set('gaps', '1');
|
|
247
|
+
const data = await get(client, 'quoxseo.queries.rows', { query: `?${qs}` });
|
|
248
|
+
if (jsonOut(data)) return;
|
|
249
|
+
if (!data.crawl_id) { console.log('No Search Console query data yet; the ingest with Search Console access has not run.'); return; }
|
|
250
|
+
const p = data.period;
|
|
251
|
+
const when = p && p.start ? `${p.start}..${p.end}` : 'period unknown';
|
|
252
|
+
const rows = data.rows || [];
|
|
253
|
+
if (data.view === 'topic-gaps') {
|
|
254
|
+
console.log(`topic gaps (impressions, no page targets the query) · ${when} · ${rows.length} of ${data.total_gaps}`);
|
|
255
|
+
} else {
|
|
256
|
+
console.log(`queries by ${data.sort}${opts.filter ? ` matching "${opts.filter}"` : ''} · ${when} · ${rows.length} of ${data.matched}`);
|
|
257
|
+
}
|
|
258
|
+
if (!rows.length) { console.log('No rows.'); return; }
|
|
259
|
+
for (const r of rows) {
|
|
260
|
+
console.log(`${r.query} | ${r.page} impr ${r.impressions} clk ${r.clicks} ctr ${(r.ctr * 100).toFixed(1)}% pos ${r.position ?? '-'}`);
|
|
261
|
+
}
|
|
262
|
+
} catch (err) { annotate(err); }
|
|
263
|
+
});
|
|
264
|
+
|
|
234
265
|
s.command('coverage')
|
|
235
266
|
.description("Google index coverage: what's indexed, and where Google disagrees with the page")
|
|
236
267
|
.option('--filter <f>', 'indexed | not-indexed')
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quox research domains list|add <domain>|remove <domain>
|
|
3
|
+
*
|
|
4
|
+
* The curated trusted research list: hosts the research agent may web_read after a web_search has run.
|
|
5
|
+
* Stored as the org setting RESEARCH_TRUSTED_DOMAINS through the SAME governed settings path as
|
|
6
|
+
* `quox settings set` (PATCH /api/v1/platform/settings/flags/:key, human org admin, witnessed there).
|
|
7
|
+
* Unset or empty means the built-in default list, which add/remove seed from so a first edit never drops it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
11
|
+
|
|
12
|
+
const KEY = 'RESEARCH_TRUSTED_DOMAINS';
|
|
13
|
+
|
|
14
|
+
// Mirror of services/collector/lib/researchTrustedDefaults.js DEFAULT_TRUSTED_SETTING (a test keeps them identical).
|
|
15
|
+
export const DEFAULT_DOMAINS = (`nist.gov,cisa.gov,cve.org,=cve.mitre.org,=cveawg.mitre.org,=www.kernel.org,=developers.cloudflare.com,=attack.mitre.org,=www.python.org,=docs.python.org,=peps.python.org,=go.dev,=tip.golang.org,` +
|
|
16
|
+
`=nginx.org/en/,=nginx.org/license,=www.postgresql.org/docs/,=www.postgresql.org/support/security/,=www.postgresql.org/about/news/,` +
|
|
17
|
+
`openssl-library.org,openssl.org,openssh.com,=kubernetes.io,msrc.microsoft.com,=www.rfc-editor.org,=datatracker.ietf.org/doc/,` +
|
|
18
|
+
`security-tracker.debian.org,ubuntu.com/security,github.com/advisories/ghsa-`).split(',');
|
|
19
|
+
|
|
20
|
+
const tokens = (v) => String(v || '').split(/[\s,]+/).map((t) => t.trim().toLowerCase()).filter(Boolean);
|
|
21
|
+
|
|
22
|
+
function cleanDomain(raw) {
|
|
23
|
+
const eq = String(raw || '').trim().startsWith('=') ? '=' : '';
|
|
24
|
+
const t = String(raw || '').trim().replace(/^=/, '').toLowerCase().replace(/^https?:\/\//, '').replace(/\/+$/, '');
|
|
25
|
+
const host = t.split('/')[0].replace(/\.+$/, '');
|
|
26
|
+
if (!/^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/.test(host)) return null; // IP literals and junk refused
|
|
27
|
+
return eq + (t.includes('/') ? host + t.slice(t.indexOf('/')) : host);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function effective(client) {
|
|
31
|
+
const ep = resolveEndpoint('v1.settings.list');
|
|
32
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
33
|
+
const f = (data.flags || []).find((x) => x.key === KEY);
|
|
34
|
+
const list = tokens(f && f.value);
|
|
35
|
+
const isOrg = !!(f && f.source === 'setting' && list.length);
|
|
36
|
+
return { list: isOrg ? list : DEFAULT_DOMAINS.slice(), source: isOrg ? 'org setting' : 'built-in default' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function write(client, list) {
|
|
40
|
+
const ep = resolveEndpoint('v1.settings.set', { key: KEY });
|
|
41
|
+
return client.patch(ep.path, { service: ep.service, body: { value: list.join(',') } });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function registerResearchCommands(program, ctx) {
|
|
45
|
+
const { client } = ctx;
|
|
46
|
+
const r = program.command('research').description('Governed web research settings');
|
|
47
|
+
const d = r.command('domains').description('Trusted research domains: hosts agents may read after a web search (org setting)');
|
|
48
|
+
|
|
49
|
+
d.command('list', { isDefault: true })
|
|
50
|
+
.description('Show the effective trusted research list and where it comes from')
|
|
51
|
+
.action(async () => {
|
|
52
|
+
const { list, source } = await effective(client);
|
|
53
|
+
process.stdout.write(`source: ${source}\n`);
|
|
54
|
+
for (const e of list) process.stdout.write(` ${e}\n`);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
d.command('add <domain>')
|
|
58
|
+
.description('Add a domain (or host/path-prefix; a leading = means exact host only) to the org list (human org admin)')
|
|
59
|
+
.action(async (domain) => {
|
|
60
|
+
const dom = cleanDomain(domain);
|
|
61
|
+
if (!dom) { process.stderr.write(`Not added: "${domain}" is not a domain name (IP literals are never trusted)\n`); process.exitCode = 1; return; }
|
|
62
|
+
const { list } = await effective(client);
|
|
63
|
+
if (list.includes(dom)) { process.stdout.write(`${dom} is already on the list\n`); return; }
|
|
64
|
+
await write(client, [...list, dom]);
|
|
65
|
+
process.stdout.write(`added ${dom} (${list.length + 1} entries, org setting)\n`);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
d.command('remove <domain>')
|
|
69
|
+
.description('Remove a domain from the org list (human org admin)')
|
|
70
|
+
.action(async (domain) => {
|
|
71
|
+
const dom = cleanDomain(domain);
|
|
72
|
+
const { list } = await effective(client);
|
|
73
|
+
if (!dom || !list.includes(dom)) { process.stderr.write(`Not removed: ${domain} is not on the list\n`); process.exitCode = 1; return; }
|
|
74
|
+
const next = list.filter((e) => e !== dom);
|
|
75
|
+
if (!next.length) { process.stderr.write('Not removed: that would empty the list, which means the built-in default. Add another entry first.\n'); process.exitCode = 1; return; }
|
|
76
|
+
await write(client, next);
|
|
77
|
+
process.stdout.write(`removed ${dom} (${next.length} entries, org setting)\n`);
|
|
78
|
+
});
|
|
79
|
+
}
|
package/lib/commands/rows.js
CHANGED
|
@@ -38,9 +38,16 @@ export const CONTRACT = {
|
|
|
38
38
|
trend: 'optional. up, down, flat, or a number (the change: 3 shows ▲3, -2 shows ▼2).',
|
|
39
39
|
link: 'optional. An http(s) URL to look at for more; shown by quox rows test and /row test.'
|
|
40
40
|
},
|
|
41
|
+
// TUI-QPE 2026-10-19: additive v1.1 fields, kept apart so the v1 field list stays exactly as published.
|
|
42
|
+
fields_v1_1: {
|
|
43
|
+
cells: 'optional. [{parts: [{text, tone, bold, keep, group}], keep}]: one row in several colours. A narrow window drops the lowest keep first (a whole part, its group, or a cell; no keep = never dropped), never half a word.',
|
|
44
|
+
grid: 'optional. A short name; rows with the same grid line their cells up in columns (quox-qpe-row uses "qpe").',
|
|
45
|
+
min_cols: 'optional. A whole number; the row hides when the drawn width (the terminal width less one) is under it.'
|
|
46
|
+
},
|
|
41
47
|
example: { label: 'Approvals', value: 3, text: 'waiting for you', tone: 'warn', trend: 1, link: 'https://quox.example/approvals' },
|
|
42
48
|
rules: [
|
|
43
49
|
'Plain text output counts as {"text": "<the text>"}.',
|
|
50
|
+
'One source can draw several lines: {"text": "<short line for older clients>", "grid": "...", "lines": [<status row>, ...]} (up to 4 lines in all above the status bar).',
|
|
44
51
|
'Terminal escapes and control characters are removed before anything is drawn.',
|
|
45
52
|
'A row definition never carries a secret: use a quox or plugin row (your session is used) or a local command that reads its own secret.',
|
|
46
53
|
'Command rows run on the machine that holds them only. They are never synced, shared or imported without a person saying yes to the exact command.'
|
|
@@ -347,6 +354,8 @@ export function registerRowsCommands(program, ctx) {
|
|
|
347
354
|
if (json()) return say(JSON.stringify(CONTRACT, null, 2));
|
|
348
355
|
say(tint('heading').bold('Status-row contract v1'));
|
|
349
356
|
for (const [k, v] of Object.entries(CONTRACT.fields)) say(` ${tint('id').bold(k.padEnd(6))} ${v}`);
|
|
357
|
+
say(tint('label')('v1.1 (colours and narrow windows):'));
|
|
358
|
+
for (const [k, v] of Object.entries(CONTRACT.fields_v1_1)) say(` ${tint('id').bold(k.padEnd(8))} ${v}`);
|
|
350
359
|
say(tint('label')('Example:'));
|
|
351
360
|
say(' ' + JSON.stringify(CONTRACT.example));
|
|
352
361
|
say(tint('label')('Draws as:'));
|
package/lib/commands/run.js
CHANGED
|
@@ -39,7 +39,8 @@ export function registerRunCommands(program, ctx) {
|
|
|
39
39
|
let items = data.data || data.items || (Array.isArray(data) ? data : []);
|
|
40
40
|
// an older engine that ignores the filter must still never show another workflow's runs
|
|
41
41
|
if (options.workflow) items = items.filter((it) => (it.workflowId ?? it.workflow_id) === options.workflow);
|
|
42
|
-
rows
|
|
42
|
+
// quoxflow rows are camelCase (workflowName, startedAt, completedAt); the table reads snake_case columns.
|
|
43
|
+
rows = items.map((item) => ({ ...item, workflow_name: item.workflow_name ?? item.workflowName, started_at: item.started_at ?? item.startedAt, finished_at: item.finished_at ?? item.completedAt ?? item.finishedAt, source: 'engine' }));
|
|
43
44
|
source = 'engine';
|
|
44
45
|
} catch (err) {
|
|
45
46
|
printWarning(`Engine (quoxflow) execution list failed, falling back to the auth-service store: ${err.message}`);
|
|
@@ -90,7 +91,24 @@ export function registerRunCommands(program, ctx) {
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
// RUNS: surface WHICH step failed and the retry hint at the top level, so a failed
|
|
95
|
+
// run no longer reads as a bare "fetch failed" with no step.
|
|
96
|
+
const detail = { ...data, source };
|
|
97
|
+
const d = data.error && typeof data.error.details === 'object' && data.error.details ? data.error.details : null;
|
|
98
|
+
if (data.status === 'failed' && d && d.nodeId) {
|
|
99
|
+
detail.failedStep = d.nodeName && d.nodeName !== d.nodeId ? `${d.nodeName} (${d.nodeId})` : String(d.nodeId);
|
|
100
|
+
}
|
|
101
|
+
if (data.status === 'failed' && data.error && data.error.message) {
|
|
102
|
+
// formatDetail truncates long nested values; show the full code + network detail here.
|
|
103
|
+
detail.failure = `${data.error.code || 'ERROR'}: ${data.error.message}`;
|
|
104
|
+
}
|
|
105
|
+
if (data.metadata && data.metadata.retryable) {
|
|
106
|
+
detail.retry = data.metadata.retryExecutionId
|
|
107
|
+
? `auto-retried as ${data.metadata.retryExecutionId}`
|
|
108
|
+
: `retryable: ${data.metadata.retryHint || `quox run create --workflow ${data.workflowId}`}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
formatDetail(detail, { format: program.opts().output, full: true });
|
|
94
112
|
});
|
|
95
113
|
|
|
96
114
|
run
|
package/lib/commands/scope.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Scopes are read-only projections of the org/division/department/team
|
|
7
7
|
* hierarchy — there is no backend write route for scopes. To create,
|
|
8
|
-
* update, or delete a scope, use `quox org`, `quox
|
|
8
|
+
* update, or delete a scope, use `quox org`, `quox org divisions`,
|
|
9
9
|
* `quox department`, or `quox team` instead.
|
|
10
10
|
*/
|
|
11
11
|
|
|
@@ -18,7 +18,7 @@ export function registerScopeCommands(program, ctx) {
|
|
|
18
18
|
|
|
19
19
|
const scope = program
|
|
20
20
|
.command('scope')
|
|
21
|
-
.description('View scopes (read-only — use `quox org`/`quox
|
|
21
|
+
.description('View scopes (read-only — use `quox org`/`quox org divisions`/`quox department`/`quox team` to manage)');
|
|
22
22
|
|
|
23
23
|
scope
|
|
24
24
|
.command('list')
|
package/lib/commands/server.js
CHANGED
|
@@ -24,6 +24,8 @@ import { planUninstall, runUninstall } from '../server/uninstall.js';
|
|
|
24
24
|
import { serverChecks } from '../server/doctor.js';
|
|
25
25
|
import { writeDiagBundle } from '../server/diag.js';
|
|
26
26
|
import { latestSnapshot, listSnapshots } from '../server/snapshot.js';
|
|
27
|
+
import { runInstallFlow } from '../server/installFlow.js';
|
|
28
|
+
import { loadRegistry, saveServer, buildRows, planGo, hereIdentity, registryPath } from '../server/devServers.js';
|
|
27
29
|
|
|
28
30
|
const icon = (s) => (s === 'pass' ? chalk.green('✔') : s === 'warn' ? chalk.yellow('⚠') : s === 'skip' ? chalk.dim('·') : chalk.red('✖'));
|
|
29
31
|
|
|
@@ -69,6 +71,42 @@ export function registerServerCommands(program, ctx = {}) {
|
|
|
69
71
|
const server = program.command('server').description('Your self-hosted Quox install: status, update, rollback, uninstall, doctor, diag');
|
|
70
72
|
const withDir = (c) => c.option('--dir <path>', 'the install directory (default: the one Getting started recorded)');
|
|
71
73
|
|
|
74
|
+
server.command('install')
|
|
75
|
+
.description('Install Quox on this machine: checks, download, start, owner account, one-time dashboard link (the same installer Getting started runs)')
|
|
76
|
+
.option('--dir <path>', 'where to install (default: ~/quox-install)')
|
|
77
|
+
.option('--dry-run', 'check this machine, print the plan, change nothing')
|
|
78
|
+
.option('-y, --yes', 'skip the confirmation (required without a terminal)')
|
|
79
|
+
.option('--json', 'one JSON object per line: stage events, then a result')
|
|
80
|
+
.option('--email <address>', 'owner account email (asked when there is a terminal)')
|
|
81
|
+
.option('--password-stdin', 'read the owner password from stdin, one line (never an argument)')
|
|
82
|
+
.option('--print-login-link', 'print the live one-time sign-in link (default: only on an interactive terminal, never in --json)')
|
|
83
|
+
.action(async (o) => {
|
|
84
|
+
const asJson = Boolean(o.json) || json();
|
|
85
|
+
const human = (e) => {
|
|
86
|
+
if (e.type === 'log') { console.log(chalk.dim(` ${e.line}`)); return; }
|
|
87
|
+
if (e.type === 'stage') {
|
|
88
|
+
if (e.status === 'running') console.log(` ${chalk.cyan('◐')} ${e.stage}`);
|
|
89
|
+
else if (e.status === 'ok') console.log(` ${chalk.green('✔')} ${e.stage}${e.detail ? chalk.dim(` ${e.detail}`) : ''}`);
|
|
90
|
+
else if (e.status === 'skip') console.log(` ${chalk.dim('·')} ${e.stage} skipped${e.detail ? `: ${e.detail}` : ''}${e.next ? `. ${e.next}` : ''}`);
|
|
91
|
+
else if (e.status === 'fail') { console.log(chalk.red(` ✖ ${e.stage} failed: ${e.error}`)); console.log(` Next: ${e.next}`); }
|
|
92
|
+
} else if (e.type === 'plan') {
|
|
93
|
+
console.log(chalk.bold(`\n Plan: install Quox into ${e.dir || '~/quox-install'}`));
|
|
94
|
+
for (const c of e.checks) console.log(` ${c.status === 'fail' ? chalk.red('✖') : c.status === 'warn' ? chalk.yellow('⚠') : chalk.green('✔')} ${c.id.padEnd(8)} ${c.body}`);
|
|
95
|
+
console.log(' It will:'); e.steps.forEach((s, i) => console.log(` ${i + 1}. ${s}`));
|
|
96
|
+
console.log(chalk.dim('\n Dry run: nothing was changed. Run quox server install to install.\n'));
|
|
97
|
+
} else if (e.type === 'result' && e.ok && !e.dryRun) {
|
|
98
|
+
console.log(chalk.green('\n ✔ Your Quox is running.'));
|
|
99
|
+
if (e.link) console.log(` Dashboard sign-in link (single use, 10 minutes): ${e.link}`);
|
|
100
|
+
else console.log(` A one-time sign-in link was issued but not printed (stdout is not a terminal). Get one with: ${e.linkHow}`);
|
|
101
|
+
console.log(chalk.dim(' Your browser will warn about the certificate once: it is the self-signed one this install made.\n'));
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const emit = (e) => (asJson ? console.log(JSON.stringify(e)) : human(e));
|
|
105
|
+
const r = await runInstallFlow({ dir: o.dir, dryRun: dry(o), yes: yes(o), email: o.email, passwordStdin: Boolean(o.passwordStdin), printLink: Boolean(o.printLoginLink) || (!asJson && Boolean(process.stdout.isTTY)) }, deps.installFlowDeps || {}, emit);
|
|
106
|
+
if (!r.ok && !r.dryRun) process.exitCode = r.declined ? 1 : 2;
|
|
107
|
+
if (!r.ok && r.dryRun === undefined && r.failedStage && dry(o)) process.exitCode = 2;
|
|
108
|
+
});
|
|
109
|
+
|
|
72
110
|
withDir(server.command('status').description('Version, health per service, disk, ports and the last update (read-only)'))
|
|
73
111
|
.action(async (o) => {
|
|
74
112
|
const i = await install(o);
|
|
@@ -288,5 +326,117 @@ export function registerServerCommands(program, ctx = {}) {
|
|
|
288
326
|
console.log(`Private-network targets: ${r.enabled ? 'on' : 'off'} ${chalk.dim('(quox server config lan-targets on|off)')}`);
|
|
289
327
|
});
|
|
290
328
|
|
|
329
|
+
// DEVFLEET CX-2: `quox server roles` — the server roles registry (role -> url). A role (brain today) can be pointed
|
|
330
|
+
// off-box so the service runs on another host without an env edit; resolveServiceUrl reads the registry before env.
|
|
331
|
+
// list is readable by any instance admin; set/unset mutate instance-wide state so are instance-OWNER (superadmin) only.
|
|
332
|
+
const roles = server.command('roles').description('Point a server role (e.g. brain) at a URL, so a service can run off-box without an env change');
|
|
333
|
+
roles.command('list')
|
|
334
|
+
.description('Show each role, its URL, and whether it comes from the registry or the environment (read-only)')
|
|
335
|
+
.action(async () => {
|
|
336
|
+
const { client, auth } = ctx;
|
|
337
|
+
auth?.requireAuth?.();
|
|
338
|
+
const ep = resolveEndpoint('server.roles.list');
|
|
339
|
+
const r = await client.get(ep.path, { service: ep.service });
|
|
340
|
+
if (json()) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
341
|
+
if (r.corrupt) console.log(chalk.yellow(` ⚠ the roles store is unreadable (${r.error}); showing environment values only
|
|
342
|
+
`));
|
|
343
|
+
for (const row of (r.roles || [])) {
|
|
344
|
+
const where = row.source === 'registry' ? chalk.green('registry') : (row.source === 'env' ? chalk.dim('environment') : chalk.dim('unset'));
|
|
345
|
+
console.log(` ${row.role.padEnd(10)} ${row.url || chalk.dim('(none)')} ${chalk.dim(`[${where}]`)}`);
|
|
346
|
+
}
|
|
347
|
+
if (!(r.roles || []).length) console.log(chalk.dim(' no roles'));
|
|
348
|
+
});
|
|
349
|
+
roles.command('set')
|
|
350
|
+
.description('Point a role at a URL (instance owner only). Use --test to probe it for health and version first')
|
|
351
|
+
.argument('<role>', 'the role, e.g. brain')
|
|
352
|
+
.argument('<url>', 'the URL the role should resolve to, e.g. http://10.1.2.3:3106')
|
|
353
|
+
.option('--test', 'probe <url> for health and version before storing it; abort if it is unreachable')
|
|
354
|
+
.action(async (role, url, o) => {
|
|
355
|
+
const { client, auth } = ctx;
|
|
356
|
+
auth?.requireAuth?.();
|
|
357
|
+
if (o.test) {
|
|
358
|
+
const tp = resolveEndpoint('server.roles.test', { role });
|
|
359
|
+
const t = await client.post(tp.path, { service: tp.service, body: { url } });
|
|
360
|
+
if (!t.success) { const e = new Error(`--test failed: ${t.error || `status ${t.status}`}; not storing`); e.exitCode = 1; throw e; }
|
|
361
|
+
if (!json()) console.log(chalk.dim(` probe ok: status ${t.status}${t.version ? `, version ${t.version}` : ''}`));
|
|
362
|
+
}
|
|
363
|
+
const ep = resolveEndpoint('server.roles.set', { role });
|
|
364
|
+
const r = await client.put(ep.path, { service: ep.service, body: { url } });
|
|
365
|
+
if (json()) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
366
|
+
console.log(chalk.green(` ✔ role ${r.role} -> ${r.url}`));
|
|
367
|
+
});
|
|
368
|
+
roles.command('unset')
|
|
369
|
+
.description('Remove a role from the registry so it falls back to the environment value (instance owner only)')
|
|
370
|
+
.argument('<role>', 'the role to remove')
|
|
371
|
+
.action(async (role) => {
|
|
372
|
+
const { client, auth } = ctx;
|
|
373
|
+
auth?.requireAuth?.();
|
|
374
|
+
const ep = resolveEndpoint('server.roles.unset', { role });
|
|
375
|
+
const r = await client.delete(ep.path, { service: ep.service });
|
|
376
|
+
if (json()) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
377
|
+
console.log(chalk.green(r.removed ? ` ✔ role ${role} removed; falling back to the environment value` : ` role ${role} was not set`));
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// DEVFLEET: the registered dev servers. Bastion supplies address + state, the org's dev-server registry supplies purpose/project/workdir.
|
|
381
|
+
const regPath = () => deps.registryPath || registryPath(env);
|
|
382
|
+
async function devRows() {
|
|
383
|
+
const { client, auth } = ctx;
|
|
384
|
+
auth?.requireAuth?.();
|
|
385
|
+
const ep = resolveEndpoint('v1.fleet.hosts.list');
|
|
386
|
+
const d = await client.get(ep.path, { service: ep.service, retries: 0, timeout: 5000 });
|
|
387
|
+
const hosts = d?.hosts || d;
|
|
388
|
+
if (!Array.isArray(hosts)) throw new Error('could not read the Bastion host registry (quox fleet hosts)');
|
|
389
|
+
{ const reg = await loadRegistry({ client, path: regPath() }); return buildRows(hosts, { servers: reg.servers, defaults: reg.defaults }, deps.here || hereIdentity()); }
|
|
390
|
+
}
|
|
391
|
+
server.command('list')
|
|
392
|
+
.description('The registered dev servers: purpose, project, reachable, and which one this is (read-only)')
|
|
393
|
+
.action(async () => {
|
|
394
|
+
const rows = await devRows();
|
|
395
|
+
if (json()) { console.log(JSON.stringify(rows.map(({ ip, user, jump, ...r }) => r), null, 2)); return; }
|
|
396
|
+
if (!rows.length) { console.log(chalk.dim(` no dev servers registered. Add one: quox server mark <host> --purpose "main dev"`)); return; }
|
|
397
|
+
const showDeploy = rows.some(r => r.lastDeploy);
|
|
398
|
+
const cell = (v) => (v == null || v === '' ? '-' : String(v));
|
|
399
|
+
const w = (k) => Math.max(k.length, ...rows.map(r => cell(r[k]).length));
|
|
400
|
+
const cols = ['name', 'purpose', 'project', 'repo'];
|
|
401
|
+
const widths = Object.fromEntries(cols.map(k => [k, w(k)]));
|
|
402
|
+
console.log(chalk.bold(` ${cols.map(k => k.toUpperCase().padEnd(widths[k])).join(' ')} REACHABLE${showDeploy ? ' LAST DEPLOY' : ''}`));
|
|
403
|
+
for (const r of rows) {
|
|
404
|
+
const line = `${cols.map(k => cell(r[k]).padEnd(widths[k])).join(' ')} ${(r.reachable ? 'yes' : 'no').padEnd(9)}${showDeploy ? ` ${cell(r.lastDeploy)}` : ''}`;
|
|
405
|
+
console.log(` ${line}${r.current ? chalk.cyan(' <- this server') : ''}`);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
server.command('mark')
|
|
409
|
+
.description('Record what a dev server is for (org registry on the Quox server, additive): purpose, project, repo, workdir, ci')
|
|
410
|
+
.argument('<name>', 'the Bastion host id, e.g. web01')
|
|
411
|
+
.option('--purpose <text>', 'what this server is for, e.g. "main dev"')
|
|
412
|
+
.option('--project <text>').option('--repo <text>').option('--workdir <path>', 'where go starts quox on that server')
|
|
413
|
+
.option('--ci <text>', 'CI or auto-deploy note or URL').option('--user <user>', 'ssh user (default control)').option('--jump <host>', 'ssh ProxyJump alias (default: the registry defaults.jump, else a direct ssh)')
|
|
414
|
+
.option('--last-deploy <text>', 'last deploy note (shown only if any server has one)')
|
|
415
|
+
.action(async (name, o) => {
|
|
416
|
+
ctx.auth?.requireAuth?.();
|
|
417
|
+
const e = await saveServer({ client: ctx.client, name, fields: { ...o, lastDeploy: o.lastDeploy }, path: regPath() });
|
|
418
|
+
if (json()) { console.log(JSON.stringify({ name, ...e }, null, 2)); return; }
|
|
419
|
+
console.log(chalk.green(` ✔ ${name} recorded${e.purpose ? ` as ${e.purpose}` : ''}`));
|
|
420
|
+
});
|
|
421
|
+
server.command('go')
|
|
422
|
+
.description('Open a Quox window on a registered dev server (ssh via your own config and the Bastion jump host, then quox in its workdir)')
|
|
423
|
+
.argument('<name>', 'a server from `quox server list`')
|
|
424
|
+
.option('--dry-run', 'print what would run and run nothing')
|
|
425
|
+
.action(async (name, o) => {
|
|
426
|
+
const rows = await devRows();
|
|
427
|
+
const plan = planGo(rows.find(r => r.name === name));
|
|
428
|
+
if (plan.error) {
|
|
429
|
+
const known = rows.map(r => r.name).join(', ') || 'none registered';
|
|
430
|
+
const e = new Error(plan.error === 'unknown server' ? `unknown server "${name}". Registered: ${known}` : plan.error);
|
|
431
|
+
e.exitCode = 1; throw e;
|
|
432
|
+
}
|
|
433
|
+
console.log(chalk.dim(` going to ${name}: ${plan.text}`));
|
|
434
|
+
if (dry(o)) return;
|
|
435
|
+
const spawnImpl = deps.spawnSync || (await import('node:child_process')).spawnSync;
|
|
436
|
+
const r = spawnImpl(plan.cmd, plan.args, { stdio: 'inherit', cwd: plan.cwd });
|
|
437
|
+
if (r?.error) { const e = new Error(`could not start ${plan.cmd}: ${r.error.message}`); e.exitCode = 1; throw e; }
|
|
438
|
+
if (r?.status) process.exitCode = r.status;
|
|
439
|
+
});
|
|
440
|
+
|
|
291
441
|
return server;
|
|
292
442
|
}
|