quoxcli 0.2.0 → 0.3.1
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/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/admin.js +26 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/contactsq.js +2 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +30 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +6 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +86 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +163 -27
- package/lib/commands/agentAttach.js +94 -6
- package/lib/commands/approvals.js +73 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +57 -2
- package/lib/commands/claude.js +34 -8
- package/lib/commands/contacts.js +49 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +17 -6
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +311 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +178 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/licence.js +14 -155
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +11 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +11 -1
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +38 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +64 -9
- package/lib/commands/server.js +292 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/skills.js +48 -5
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/core/actingAs.js +2 -2
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +144 -35
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/config.js +142 -24
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/global-flags.js +105 -5
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +4 -4
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +504 -25
- package/lib/quoxon/app.js +3000 -230
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +363 -38
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +44 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +244 -25
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +49 -6
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +92 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +24 -3
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +9 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +58 -37
- package/lib/quoxon/render.js +638 -119
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +95 -9
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +172 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +21 -7
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/restart.js +71 -13
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +4 -1
- package/lib/core/websiteSession.js +0 -148
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* quox server: lifecycle of a local self-hosted install (plan C4).
|
|
3
|
+
*
|
|
4
|
+
* status version, health per service, disk, ports, last update (read-only)
|
|
5
|
+
* update current -> latest public quoxcore tag; backup first; fetch, build, migrate, health;
|
|
6
|
+
* auto-rollback on failure; resumable; --dry-run prints the plan and runs nothing
|
|
7
|
+
* rollback previous snapshot + previous version; --dry-run
|
|
8
|
+
* uninstall lists exactly what goes, two-step confirmation, keeps backups unless --purge; --dry-run
|
|
9
|
+
* doctor the quox doctor checks + server-side checks (read-only)
|
|
10
|
+
* diag a redacted support bundle written locally (allowlist redaction), never uploaded
|
|
11
|
+
*
|
|
12
|
+
* Every mutating host command goes through lib/server/sys.js gate(): in a dry run it is printed, never run.
|
|
13
|
+
* The install is the one Getting started recorded (or --dir / QUOX_INSTALL_DIR / the running containers).
|
|
14
|
+
*/
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { resolve } from 'node:path';
|
|
18
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
19
|
+
import { createSys } from '../server/sys.js';
|
|
20
|
+
import { resolveInstall, backupsRoot, readInstalled, fetchAvailable, cmpSemver, vtag, compareUrl, releaseNotesUrl } from '../server/install.js';
|
|
21
|
+
import { gatherStatus, statusRows, defaultUrls } from '../server/status.js';
|
|
22
|
+
import { runUpdate, runRollback, UPDATE_STEPS } from '../server/update.js';
|
|
23
|
+
import { planUninstall, runUninstall } from '../server/uninstall.js';
|
|
24
|
+
import { serverChecks } from '../server/doctor.js';
|
|
25
|
+
import { writeDiagBundle } from '../server/diag.js';
|
|
26
|
+
import { latestSnapshot, listSnapshots } from '../server/snapshot.js';
|
|
27
|
+
|
|
28
|
+
const icon = (s) => (s === 'pass' ? chalk.green('✔') : s === 'warn' ? chalk.yellow('⚠') : s === 'skip' ? chalk.dim('·') : chalk.red('✖'));
|
|
29
|
+
|
|
30
|
+
/** Pure: is the two-step uninstall confirmation satisfied? */
|
|
31
|
+
export function uninstallConfirmed({ word, path, root }) {
|
|
32
|
+
return String(word || '').trim() === 'uninstall' && resolve(String(path || '').trim() || '/nonexistent') === resolve(root);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function printRows(rows, out = console.log) {
|
|
36
|
+
for (const r of rows) {
|
|
37
|
+
if (r.kind === 'head') out(chalk.bold(`\n ${r.text}`));
|
|
38
|
+
else if (r.kind === 'gap') out('');
|
|
39
|
+
else if (r.kind === 'dim') out(chalk.dim(` ${r.key ? `${String(r.key).padEnd(16)} ` : ''}${r.text}`));
|
|
40
|
+
else out(` ${String(r.key).padEnd(16)} ${r.text}`);
|
|
41
|
+
}
|
|
42
|
+
out('');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function registerServerCommands(program, ctx = {}) {
|
|
46
|
+
const deps = ctx.serverDeps || {};
|
|
47
|
+
const env = deps.env || process.env;
|
|
48
|
+
const sys = deps.sys || createSys();
|
|
49
|
+
const fetchImpl = deps.fetchImpl || ((...a) => globalThis.fetch(...a));
|
|
50
|
+
const backups = deps.backups || backupsRoot(env);
|
|
51
|
+
const log = deps.log || ((l) => console.log(chalk.dim(` ${l}`)));
|
|
52
|
+
const ask = deps.ask || (async (message) => {
|
|
53
|
+
const { default: inquirer } = await import('inquirer');
|
|
54
|
+
const a = await inquirer.prompt([{ type: 'input', name: 'v', message }]);
|
|
55
|
+
return a.v;
|
|
56
|
+
});
|
|
57
|
+
const isTTY = deps.isTTY ?? (() => Boolean(process.stdin.isTTY));
|
|
58
|
+
const urls = () => deps.urls || defaultUrls(ctx.config);
|
|
59
|
+
const json = () => program.opts().output === 'json' || program.opts().json;
|
|
60
|
+
const dry = (o) => Boolean(o.dryRun || program.opts().dryRun);
|
|
61
|
+
const yes = (o) => Boolean(o.yes || program.opts().yes);
|
|
62
|
+
|
|
63
|
+
async function install(o) {
|
|
64
|
+
const i = await resolveInstall({ dir: o.dir, env, sys, journey: deps.journey });
|
|
65
|
+
if (i.error) { const e = new Error(i.error); e.exitCode = 2; throw e; }
|
|
66
|
+
return i;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const server = program.command('server').description('Your self-hosted Quox install: status, update, rollback, uninstall, doctor, diag');
|
|
70
|
+
const withDir = (c) => c.option('--dir <path>', 'the install directory (default: the one Getting started recorded)');
|
|
71
|
+
|
|
72
|
+
withDir(server.command('status').description('Version, health per service, disk, ports and the last update (read-only)'))
|
|
73
|
+
.action(async (o) => {
|
|
74
|
+
const i = await install(o);
|
|
75
|
+
const st = await gatherStatus({ install: i, sys, fetchImpl, urls: urls(), backups });
|
|
76
|
+
if (json()) { console.log(JSON.stringify(st, null, 2)); return; }
|
|
77
|
+
printRows(statusRows(st));
|
|
78
|
+
if (!st.http.every((h) => h.ok) || (st.services.ok && st.services.services.some((s) => s.state !== 'running'))) process.exitCode = 1;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
withDir(server.command('update').description('Update to the latest release: backup first, then fetch, build, migrate, health; rolls back on failure'))
|
|
82
|
+
.option('--dry-run', 'print the plan and run nothing')
|
|
83
|
+
.option('--to <version>', 'a specific release (default: the latest quoxcore tag)')
|
|
84
|
+
.option('--force', 'proceed despite local changes or low disk, or re-apply the same version')
|
|
85
|
+
.option('--restart', 'abandon a stopped update to a different version and start this one')
|
|
86
|
+
.option('--no-rollback', 'on failure, stop where it failed instead of rolling back')
|
|
87
|
+
.option('-y, --yes', 'skip the confirmation')
|
|
88
|
+
.action(async (o) => {
|
|
89
|
+
const i = await install(o);
|
|
90
|
+
const dryRun = dry(o);
|
|
91
|
+
if (!dryRun && !yes(o)) {
|
|
92
|
+
const installed = await readInstalled(i, sys);
|
|
93
|
+
let target = o.to ? vtag(o.to) : null;
|
|
94
|
+
if (!target) { const av = await fetchAvailable({ fetchImpl }); if (!av.ok) throw new Error(`could not read the available versions: ${av.error}`); target = av.latest; }
|
|
95
|
+
const cur = installed.release ? vtag(installed.release) : '?';
|
|
96
|
+
if (cmpSemver(target, cur) !== null && cmpSemver(target, cur) <= 0 && !o.force) { console.log(`Already on ${cur} (latest is ${target}). Nothing to do.`); return; }
|
|
97
|
+
console.log(`\n Update ${cur} -> ${chalk.bold(target)} at ${i.repoDir}`);
|
|
98
|
+
console.log(` Changes: ${installed.release ? compareUrl(cur, target) : releaseNotesUrl(target)}`);
|
|
99
|
+
console.log(` A backup (database + config) is taken first, into ${backups}. The stack stops briefly while volumes are archived.`);
|
|
100
|
+
console.log(' Preview every command first with: quox server update --dry-run\n');
|
|
101
|
+
if (!isTTY()) throw new Error('quox server update needs confirmation; re-run with --yes in a non-interactive shell');
|
|
102
|
+
const a = await ask(`Type "update" to update to ${target}`);
|
|
103
|
+
if (String(a).trim() !== 'update') { console.log('Not updated. Nothing was changed.'); return; }
|
|
104
|
+
}
|
|
105
|
+
const r = await runUpdate({ install: i, sys, backups, urls: urls(), fetchImpl, dryRun, to: o.to, force: o.force, restart: o.restart, noRollback: o.rollback === false, log });
|
|
106
|
+
if (json()) { console.log(JSON.stringify(r, null, 2)); if (!r.ok) process.exitCode = 2; return; }
|
|
107
|
+
const p = r.plan || {};
|
|
108
|
+
if (r.upToDate) { console.log(`Already on ${p.current} (latest is ${p.target}). Nothing to do.`); return; }
|
|
109
|
+
if (dryRun && r.ok) {
|
|
110
|
+
console.log(chalk.bold(`\n Plan: ${p.current || '?'} (${p.currentCommit || '?'}) -> ${p.target}${p.targetCommit ? ` (${p.targetCommit})` : ''}`));
|
|
111
|
+
if (p.changes) console.log(` What changes: ${p.changes}`);
|
|
112
|
+
console.log(` Release notes: ${p.releaseNotes} (if published for this tag)`);
|
|
113
|
+
if (p.resumedAfter) console.log(` Resumes after: ${p.resumedAfter}`);
|
|
114
|
+
console.log(` Steps: ${UPDATE_STEPS.join(' -> ')}; on failure it rolls back automatically.`);
|
|
115
|
+
if (p.problems?.length) for (const x of p.problems) console.log(chalk.yellow(` ⚠ ${x}`));
|
|
116
|
+
console.log(chalk.dim(`\n Dry run: ${r.planned.length} changes listed above, none made. Re-run without --dry-run to update.\n`));
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (r.ok) { console.log(chalk.green(`\n ✔ Updated to ${p.target}. Backup: ${p.snapshot}\n`)); return; }
|
|
120
|
+
process.exitCode = 2;
|
|
121
|
+
console.log(chalk.red(`\n ✖ Update failed at ${r.failedStep}: ${r.error}`));
|
|
122
|
+
if (r.rollback) console.log(r.rolledBack ? chalk.yellow(` Rolled back (${r.rollback.scope}); you are on ${p.current} again.`) : chalk.red(` Rollback did not complete: ${r.rollback.error}. Backup: ${p.snapshot}. Run quox server rollback.`));
|
|
123
|
+
else if (r.failedStep !== 'preflight' && r.failedStep !== 'backup') console.log(` Not rolled back. Re-run quox server update to resume, or quox server rollback.`);
|
|
124
|
+
console.log('');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
withDir(server.command('rollback').description('Restore the previous snapshot and version (database, config, code)'))
|
|
128
|
+
.option('--dry-run', 'print the plan and run nothing')
|
|
129
|
+
.option('--snapshot <dir>', 'a specific snapshot (default: the latest)')
|
|
130
|
+
.option('--list', 'list the snapshots and exit')
|
|
131
|
+
.option('-y, --yes', 'skip the confirmation')
|
|
132
|
+
.action(async (o) => {
|
|
133
|
+
if (o.list) {
|
|
134
|
+
const s = listSnapshots(backups);
|
|
135
|
+
if (json()) { console.log(JSON.stringify(s, null, 2)); return; }
|
|
136
|
+
if (!s.length) { console.log(`No snapshots in ${backups}.`); return; }
|
|
137
|
+
for (const x of s) console.log(` ${x.createdAt} ${String(x.release || '?').padEnd(8)} ${String(x.commit || '?').padEnd(10)} ${x.reason} ${x.dir}`);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const i = await install(o);
|
|
141
|
+
const dryRun = dry(o);
|
|
142
|
+
const snap = o.snapshot ? listSnapshots(backups).find((s) => s.dir === o.snapshot || s.dir.endsWith(`/${o.snapshot}`)) : latestSnapshot(backups);
|
|
143
|
+
if (!snap) throw new Error(o.snapshot ? `no complete snapshot at ${o.snapshot}` : `no snapshot in ${backups}; nothing to roll back to`);
|
|
144
|
+
if (!dryRun && !yes(o)) {
|
|
145
|
+
console.log(`\n Roll back to ${snap.release || '?'} (${snap.commit}) from ${snap.createdAt}.`);
|
|
146
|
+
console.log(chalk.yellow(' This REPLACES the current database and config with the snapshot; anything since then is lost.'));
|
|
147
|
+
console.log(' Preview first with: quox server rollback --dry-run\n');
|
|
148
|
+
if (!isTTY()) throw new Error('quox server rollback needs confirmation; re-run with --yes in a non-interactive shell');
|
|
149
|
+
const a = await ask('Type "rollback" to continue');
|
|
150
|
+
if (String(a).trim() !== 'rollback') { console.log('Not rolled back. Nothing was changed.'); return; }
|
|
151
|
+
}
|
|
152
|
+
const r = await runRollback({ install: i, sys, backups, urls: urls(), fetchImpl, dryRun, snapshotDir: snap.dir, log });
|
|
153
|
+
if (json()) { console.log(JSON.stringify(r, null, 2)); if (!r.ok) process.exitCode = 2; return; }
|
|
154
|
+
if (r.ok && dryRun) { console.log(chalk.dim(`\n Dry run: rollback to ${snap.release || snap.commit} from ${snap.dir}; ${r.planned.length} changes listed above, none made.\n`)); return; }
|
|
155
|
+
if (r.ok) { console.log(chalk.green(`\n ✔ Rolled back to ${snap.release || snap.commit}.\n`)); return; }
|
|
156
|
+
process.exitCode = 2;
|
|
157
|
+
console.log(chalk.red(`\n ✖ Rollback failed at ${r.step}: ${r.error}\n`));
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
withDir(server.command('uninstall').description('Remove this install (lists everything first; keeps backups unless --purge)'))
|
|
161
|
+
.option('--dry-run', 'list what would be removed and run nothing')
|
|
162
|
+
.option('--purge', 'also delete every backup')
|
|
163
|
+
.option('--no-backup', 'skip the final snapshot')
|
|
164
|
+
.option('-y, --yes', 'non-interactive: confirm step one (needs --confirm too)')
|
|
165
|
+
.option('--confirm <path>', 'non-interactive: confirm step two by naming the install directory')
|
|
166
|
+
.action(async (o) => {
|
|
167
|
+
const i = await install(o);
|
|
168
|
+
const dryRun = dry(o);
|
|
169
|
+
const plan = await planUninstall({ install: i, sys, backups, purge: Boolean(o.purge), home: deps.home || homedir() });
|
|
170
|
+
if (json() && dryRun) { console.log(JSON.stringify({ plan }, null, 2)); return; }
|
|
171
|
+
console.log(chalk.bold('\n This removes:'));
|
|
172
|
+
console.log(` Containers (${plan.containers.length}): ${plan.containers.join(', ') || plan.containersError || 'none'}`);
|
|
173
|
+
console.log(` Volumes, with ALL their data (${plan.volumes.length}): ${plan.volumes.join(', ') || 'none'}`);
|
|
174
|
+
console.log(` Images built for this install (${plan.images.length}): ${plan.images.join(', ') || 'none'}`);
|
|
175
|
+
console.log(` Networks: ${plan.networks}`);
|
|
176
|
+
console.log(` Directories: ${[...plan.dirs, ...(plan.root ? [plan.root] : [])].join(', ')}`);
|
|
177
|
+
if (plan.journeyFile) console.log(` Getting started state: ${plan.journeyFile}`);
|
|
178
|
+
if (plan.refused.length) console.log(chalk.yellow(` Refusing to remove (too broad): ${plan.refused.join(', ')}`));
|
|
179
|
+
console.log(chalk.bold(' It keeps:'));
|
|
180
|
+
console.log(plan.backups.removed ? chalk.red(` Nothing: --purge also deletes ${plan.backups.count} backup(s) in ${plan.backups.dir}`)
|
|
181
|
+
: ` Backups in ${plan.backups.dir} (${plan.backups.count} now${o.backup === false ? '' : ', plus a final snapshot taken first'})`);
|
|
182
|
+
console.log('');
|
|
183
|
+
if (!dryRun) {
|
|
184
|
+
let ok;
|
|
185
|
+
if (isTTY() && !(yes(o) && o.confirm)) {
|
|
186
|
+
const w = await ask('Step 1 of 2: type "uninstall" to continue');
|
|
187
|
+
if (String(w).trim() !== 'uninstall') { console.log('Not uninstalled. Nothing was changed.'); return; }
|
|
188
|
+
const p = await ask(`Step 2 of 2: type the install directory (${i.root}) to confirm`);
|
|
189
|
+
ok = uninstallConfirmed({ word: 'uninstall', path: p, root: i.root });
|
|
190
|
+
} else ok = yes(o) && uninstallConfirmed({ word: 'uninstall', path: o.confirm, root: i.root });
|
|
191
|
+
if (!ok) {
|
|
192
|
+
if (!isTTY()) throw new Error(`quox server uninstall needs two confirmations; in a non-interactive shell pass --yes and --confirm ${i.root}`);
|
|
193
|
+
console.log('The directory did not match. Not uninstalled. Nothing was changed.');
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const r = await runUninstall({ install: i, sys, backups, purge: Boolean(o.purge), noBackup: o.backup === false, dryRun, log, plan });
|
|
198
|
+
if (json()) { console.log(JSON.stringify(r, null, 2)); if (!r.ok) process.exitCode = 2; return; }
|
|
199
|
+
if (!r.ok) { process.exitCode = 2; console.log(chalk.red(` ✖ Uninstall stopped at ${r.step}: ${r.error}\n`)); return; }
|
|
200
|
+
if (dryRun) { console.log(chalk.dim(` Dry run: ${r.planned.length} changes listed above, none made.\n`)); return; }
|
|
201
|
+
console.log(chalk.green(` ✔ Uninstalled.${r.finalSnapshot ? ` Final snapshot: ${r.finalSnapshot}` : ''}\n`));
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
withDir(server.command('doctor').description('The quox doctor checks plus server-side checks (read-only)'))
|
|
205
|
+
.action(async (o) => {
|
|
206
|
+
const i = await install(o);
|
|
207
|
+
const { runDoctorChecks } = await import('./doctor.js');
|
|
208
|
+
const platform = deps.platformChecks ? await deps.platformChecks() : await runDoctorChecks({ config: ctx.config });
|
|
209
|
+
const srv = await serverChecks({ install: i, sys, fetchImpl, urls: urls(), backups });
|
|
210
|
+
const all = [...platform, ...srv];
|
|
211
|
+
const failed = all.filter((c) => c.status === 'fail' || c.status === 'error').length;
|
|
212
|
+
const warned = all.filter((c) => c.status === 'warn').length;
|
|
213
|
+
if (json()) { console.log(JSON.stringify({ platform, server: srv, summary: { failed, warnings: warned } }, null, 2)); }
|
|
214
|
+
else {
|
|
215
|
+
console.log(chalk.bold('\n Platform'));
|
|
216
|
+
for (const c of platform) console.log(` ${icon(c.status)} ${String(c.name).padEnd(20)} ${chalk.dim(c.detail || '')}`);
|
|
217
|
+
console.log(chalk.bold('\n Server'));
|
|
218
|
+
for (const c of srv) console.log(` ${icon(c.status)} ${String(c.name).padEnd(20)} ${chalk.dim(c.detail || '')}`);
|
|
219
|
+
console.log(`\n ${all.length - failed - warned} passed · ${warned} warnings · ${failed} failed\n`);
|
|
220
|
+
}
|
|
221
|
+
process.exitCode = failed ? 2 : warned ? 1 : 0;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
withDir(server.command('diag').description('Write a redacted support bundle locally (never uploaded)'))
|
|
225
|
+
.option('--out <dir>', 'where to write it (default: the current directory)')
|
|
226
|
+
.action(async (o) => {
|
|
227
|
+
const i = await install(o);
|
|
228
|
+
const { runDoctorChecks } = await import('./doctor.js');
|
|
229
|
+
const { resolveCliVersion } = await import('../core/version.js');
|
|
230
|
+
const st = await gatherStatus({ install: i, sys, fetchImpl, urls: urls(), backups });
|
|
231
|
+
const platform = deps.platformChecks ? await deps.platformChecks() : await runDoctorChecks({ config: ctx.config }).catch(() => []);
|
|
232
|
+
const srv = await serverChecks({ install: i, sys, fetchImpl, urls: urls(), backups });
|
|
233
|
+
const r = await writeDiagBundle({
|
|
234
|
+
install: i, sys, out: resolve(o.out || process.cwd()),
|
|
235
|
+
data: { versions: { cli: resolveCliVersion(), installed: st.installed, running: st.running }, status: st, doctor: [...platform, ...srv] }
|
|
236
|
+
});
|
|
237
|
+
if (json()) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
238
|
+
console.log(chalk.green(`\n ✔ Support bundle written: ${r.file}`));
|
|
239
|
+
console.log(chalk.dim(' Nothing was uploaded. Secrets are kept out by an allowlist; look through it before you share it.\n'));
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// `quox server config app-url [url]`: read or set the public app URL (instance_config app_base_url).
|
|
243
|
+
// Server-side source of truth, the same value Settings > General > App URL edits.
|
|
244
|
+
const cfg = server.command('config').description('Instance settings stored on the server');
|
|
245
|
+
cfg.command('app-url')
|
|
246
|
+
.description('Show or set the public app URL (used in sign-in links and emails). Pass a URL to set it, or --clear to fall back to the environment value')
|
|
247
|
+
.argument('[url]', 'the new public app URL, e.g. https://quox.example.com')
|
|
248
|
+
.option('--clear', 'remove the stored value')
|
|
249
|
+
.action(async (url, o) => {
|
|
250
|
+
const { client, auth } = ctx;
|
|
251
|
+
auth?.requireAuth?.();
|
|
252
|
+
const get = resolveEndpoint('admin.instance.basics.get');
|
|
253
|
+
const set = resolveEndpoint('admin.instance.basics.set');
|
|
254
|
+
if (url && o.clear) { const e = new Error('give a URL or --clear, not both'); e.exitCode = 2; throw e; }
|
|
255
|
+
if (url || o.clear) {
|
|
256
|
+
const r = await client.put(set.path, { service: set.service, body: { appBaseUrl: o.clear ? null : url } });
|
|
257
|
+
if (json()) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
258
|
+
console.log(chalk.green(o.clear ? ' ✔ App URL cleared; using the environment value.' : ` ✔ App URL set to ${url}`));
|
|
259
|
+
console.log(chalk.dim(` Effective now: ${r.effective?.appBaseUrl}\n`));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const r = await client.get(get.path, { service: get.service });
|
|
263
|
+
if (json()) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
264
|
+
const src = r.effective?.appBaseUrlSource === 'instance_config' ? 'set on this server' : 'from the environment (not set in Settings)';
|
|
265
|
+
console.log(`${r.effective?.appBaseUrl} ${chalk.dim(`(${src})`)}`);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// `quox server config lan-targets [on|off]`: let integrations (Ollama, LM Studio, vLLM, Grafana ...)
|
|
269
|
+
// point at private-network addresses. Default off. Loopback and link-local stay blocked either way.
|
|
270
|
+
cfg.command('lan-targets')
|
|
271
|
+
.description('Show or set whether AI servers and integrations on your own private network are allowed (default off)')
|
|
272
|
+
.argument('[state]', 'on or off')
|
|
273
|
+
.action(async (state) => {
|
|
274
|
+
const { client, auth } = ctx;
|
|
275
|
+
auth?.requireAuth?.();
|
|
276
|
+
const get = resolveEndpoint('admin.instance.lan_targets.get');
|
|
277
|
+
const set = resolveEndpoint('admin.instance.lan_targets.set');
|
|
278
|
+
if (state !== undefined && !['on', 'off'].includes(String(state).toLowerCase())) { const e = new Error('give on or off'); e.exitCode = 2; throw e; }
|
|
279
|
+
if (state !== undefined) {
|
|
280
|
+
const r = await client.put(set.path, { service: set.service, body: { enabled: String(state).toLowerCase() === 'on' } });
|
|
281
|
+
if (json()) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
282
|
+
console.log(chalk.green(r.enabled ? ' ✔ Private-network targets are ON: integrations may use addresses on your own network.' : ' ✔ Private-network targets are OFF.'));
|
|
283
|
+
console.log(chalk.dim(' Loopback (localhost) and link-local addresses stay blocked either way.\n'));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const r = await client.get(get.path, { service: get.service });
|
|
287
|
+
if (json()) { console.log(JSON.stringify(r, null, 2)); return; }
|
|
288
|
+
console.log(`Private-network targets: ${r.enabled ? 'on' : 'off'} ${chalk.dim('(quox server config lan-targets on|off)')}`);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
return server;
|
|
292
|
+
}
|
package/lib/commands/setup.js
CHANGED
|
@@ -7,6 +7,16 @@ import { formatDetail, printSuccess, printInfo } from '../core/output.js';
|
|
|
7
7
|
import { resolveEndpoint } from '../api/endpoints.js';
|
|
8
8
|
import { QuoxApiError, QuoxValidationError } from '../core/errors.js';
|
|
9
9
|
import { confirmDestructive } from '../core/confirm.js';
|
|
10
|
+
import { passwordProblem } from '../quoxon/onboarding/finish.js';
|
|
11
|
+
|
|
12
|
+
// The auth service's rule (services/auth/utils/passwordValidation.js), said up front.
|
|
13
|
+
export const PASSWORD_POLICY = 'Passwords need at least 12 characters, with an uppercase letter, a lowercase letter, a number and a special character (for example - or !).';
|
|
14
|
+
|
|
15
|
+
async function readStdinText() {
|
|
16
|
+
const chunks = [];
|
|
17
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
18
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
19
|
+
}
|
|
10
20
|
|
|
11
21
|
/**
|
|
12
22
|
* LICENSE-LAUNCH LL-8: read a license-key-v2 JSON object from a file or
|
|
@@ -183,11 +193,23 @@ export function registerSetupCommands(program, ctx) {
|
|
|
183
193
|
.command('admin')
|
|
184
194
|
.description('Create the initial admin account')
|
|
185
195
|
.option('--email <email>', 'admin email')
|
|
186
|
-
.option('--password <password>', 'admin password')
|
|
196
|
+
.option('--password <password>', 'admin password (visible in the process list; prefer --password-stdin)')
|
|
197
|
+
.option('--password-stdin', 'read the admin password from stdin (first line), never argv')
|
|
187
198
|
.option('--name <name>', 'admin display name')
|
|
188
199
|
.action(async (options) => {
|
|
189
200
|
let { email, password, name } = options;
|
|
190
201
|
|
|
202
|
+
// QUOXON-Z2H lane R: argv is visible in ps and shell history, so a
|
|
203
|
+
// script pipes the password in, same as `quox login --password-stdin`.
|
|
204
|
+
if (options.password && options.passwordStdin) {
|
|
205
|
+
throw new QuoxValidationError('Pass the password with --password or --password-stdin, not both.');
|
|
206
|
+
}
|
|
207
|
+
if (options.passwordStdin) {
|
|
208
|
+
const text = await (ctx.readStdin || readStdinText)();
|
|
209
|
+
password = String(text || '').split('\n')[0].replace(/\r$/, '');
|
|
210
|
+
if (!password) throw new QuoxValidationError('--password-stdin given but stdin was empty.');
|
|
211
|
+
}
|
|
212
|
+
|
|
191
213
|
if (!email || !password || !name) {
|
|
192
214
|
const { default: inquirer } = await import('inquirer');
|
|
193
215
|
|
|
@@ -206,18 +228,22 @@ export function registerSetupCommands(program, ctx) {
|
|
|
206
228
|
}
|
|
207
229
|
if (!password) {
|
|
208
230
|
const ans = await inquirer.prompt([{
|
|
209
|
-
type: 'password', name: 'password', message:
|
|
231
|
+
type: 'password', name: 'password', message: `Password (${PASSWORD_POLICY}):`, mask: '*',
|
|
232
|
+
validate: (v) => { const bad = passwordProblem(v); return bad ? `${bad} ${PASSWORD_POLICY}` : true; },
|
|
210
233
|
}]);
|
|
211
234
|
password = ans.password;
|
|
212
235
|
}
|
|
213
236
|
}
|
|
214
237
|
|
|
238
|
+
// The policy is the FIRST thing a weak password meets, before any
|
|
239
|
+
// server call (the server enforces the same rule; this only says it sooner).
|
|
240
|
+
const problem = passwordProblem(password);
|
|
241
|
+
if (problem) throw new QuoxValidationError(`${PASSWORD_POLICY} ${problem} Nothing was sent to the server.`);
|
|
242
|
+
|
|
215
243
|
// services/auth/routes/setup.js POST /admin destructures `name` (not
|
|
216
|
-
// `display_name`) from req.body
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
// forward-compat / any other consumer that expects it.
|
|
220
|
-
const data = await client.post(resolveEndpoint('setup.admin').path, {
|
|
244
|
+
// `display_name`) from req.body. Send both: `name` is what the endpoint
|
|
245
|
+
// reads; `display_name` is kept for forward-compat.
|
|
246
|
+
await client.post(resolveEndpoint('setup.admin').path, {
|
|
221
247
|
service: resolveEndpoint('setup.admin').service,
|
|
222
248
|
body: { email, password, name, display_name: name },
|
|
223
249
|
noAuth: true,
|
package/lib/commands/skills.js
CHANGED
|
@@ -71,6 +71,38 @@ export function registerSkillsCommands(program, ctx) {
|
|
|
71
71
|
const data = await client.get(resolveEndpoint('quoxflow.skills.list').path, { service: resolveEndpoint('quoxflow.skills.list').service, query });
|
|
72
72
|
let rows = data.data || data.records || data;
|
|
73
73
|
if (!Array.isArray(rows)) rows = [];
|
|
74
|
+
rows = rows.map((r) => ({ ...r, store: 'library' }));
|
|
75
|
+
|
|
76
|
+
// 4.2.4 skills/t3-skill-view: a library skill with an active quox#748 review row (pending or rejected) cannot
|
|
77
|
+
// run, and skill_view refuses it. Say so per skill. A queue read failure leaves runnable unknown, never true.
|
|
78
|
+
try {
|
|
79
|
+
const rq = resolveEndpoint('skills.reviewQueue.list');
|
|
80
|
+
const active = new Map();
|
|
81
|
+
for (const status of ['pending', 'rejected']) {
|
|
82
|
+
const r = await client.get(rq.path, { service: rq.service, query: { status, orgId: query.orgId } });
|
|
83
|
+
const items = r && (r.data || r.records);
|
|
84
|
+
for (const it of Array.isArray(items) ? items : []) if (it && it.skill_id) active.set(it.skill_id, it.status || status);
|
|
85
|
+
}
|
|
86
|
+
rows = rows.map((r) => ({ ...r, review_status: active.get(r.id) || null, runnable: !active.has(r.id) }));
|
|
87
|
+
} catch (err) {
|
|
88
|
+
printWarning(`Skill review queue could not be read, so which skills can run is unknown: ${err.message}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// #1006: crystals (`skills from-execution`) live in quoxflow's crystal store, not this library. Merge them
|
|
92
|
+
// unless a library-only filter is set (crystals have no edition/category/agent fit). A failure there warns,
|
|
93
|
+
// it never sinks the library listing.
|
|
94
|
+
if (!options.edition && !options.category && !options.agent) {
|
|
95
|
+
try {
|
|
96
|
+
const ep = resolveEndpoint('skills.crystals.list');
|
|
97
|
+
const c = await client.get(ep.path, { service: ep.service, query: { orgId: query.orgId } });
|
|
98
|
+
let crystals = c.data || c.skills || c;
|
|
99
|
+
if (!Array.isArray(crystals)) crystals = [];
|
|
100
|
+
if (options.tag) crystals = crystals.filter((r) => Array.isArray(r.tags) && r.tags.includes(options.tag));
|
|
101
|
+
rows = rows.concat(crystals.map((r) => ({ ...r, store: 'crystal' })));
|
|
102
|
+
} catch (err) {
|
|
103
|
+
printWarning(`Crystal store (skills from-execution) could not be read, showing the library only: ${err.message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
74
106
|
|
|
75
107
|
if (options.search) {
|
|
76
108
|
const needle = options.search.toLowerCase();
|
|
@@ -84,8 +116,8 @@ export function registerSkillsCommands(program, ctx) {
|
|
|
84
116
|
// shown as undeclared, never defaulted to a licence.
|
|
85
117
|
rows = rows.map((r) => ({ ...r, licence: shortLicenceLabel(r.license) }));
|
|
86
118
|
formatOutput({
|
|
87
|
-
columns: ['id', 'name', 'category', 'edition', 'version', 'licence', 'decayState', 'createdAt'],
|
|
88
|
-
headers: ['ID', 'Name', 'Category', 'Edition', 'Version', 'Licence', 'Decay', 'Created'],
|
|
119
|
+
columns: ['id', 'store', 'name', 'category', 'edition', 'version', 'licence', 'decayState', 'createdAt'],
|
|
120
|
+
headers: ['ID', 'Store', 'Name', 'Category', 'Edition', 'Version', 'Licence', 'Decay', 'Created'],
|
|
89
121
|
rows,
|
|
90
122
|
}, { format: program.opts().output, emptyMessage: 'No skills found.' });
|
|
91
123
|
});
|
|
@@ -101,8 +133,17 @@ export function registerSkillsCommands(program, ctx) {
|
|
|
101
133
|
.action(async (id, options) => {
|
|
102
134
|
auth.requireAuth();
|
|
103
135
|
const query = { orgId: options.org || resolveOrgId(auth, program) };
|
|
104
|
-
|
|
105
|
-
|
|
136
|
+
let data, store = 'library';
|
|
137
|
+
try {
|
|
138
|
+
data = await client.get(resolveEndpoint('quoxflow.skills.get', { id }).path, { service: resolveEndpoint('quoxflow.skills.get', { id }).service, query });
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (err.status !== 404) throw err;
|
|
141
|
+
// #1006: not in the library; a crystal from `skills from-execution` lives in the crystal store
|
|
142
|
+
const ep = resolveEndpoint('skills.crystals.get', { id });
|
|
143
|
+
try { data = await client.get(ep.path, { service: ep.service, query }); } catch (err2) { if (err2.status === 404) throw err; throw err2; }
|
|
144
|
+
store = 'crystal';
|
|
145
|
+
}
|
|
146
|
+
formatDetail({ ...(data.data || data), store }, { format: program.opts().output, title: `Skill: ${id}` });
|
|
106
147
|
});
|
|
107
148
|
|
|
108
149
|
// ── skills run ───────────────────────────────────────────────────────────
|
|
@@ -354,7 +395,9 @@ export function registerSkillsCommands(program, ctx) {
|
|
|
354
395
|
const ep = resolveEndpoint('skills.rollbackToVersion');
|
|
355
396
|
const data = await client.post(ep.path, { service: ep.service, body, query: { orgId } });
|
|
356
397
|
const record = data.data || data;
|
|
357
|
-
|
|
398
|
+
// #1006: rolling back to the version that is already current is a server-side no-op (it returns that row)
|
|
399
|
+
if (record && Number(record.version) === targetVersion) printInfo(`Already at version ${targetVersion}: nothing was written.`);
|
|
400
|
+
else printSuccess(`Skill rolled back to version ${targetVersion} (written as version ${record?.version ?? '?'}).`);
|
|
358
401
|
formatDetail(record, { format: program.opts().output });
|
|
359
402
|
});
|
|
360
403
|
|