quoxcli 0.0.0-stage → 0.2.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/LICENSE +35 -0
- package/README.md +142 -2
- package/bin/quox.js +292 -0
- package/bin/quoxon.js +8 -0
- package/lib/api/endpoints/admin.js +125 -0
- package/lib/api/endpoints/aee.js +38 -0
- package/lib/api/endpoints/agent-attach.js +126 -0
- package/lib/api/endpoints/agents.js +56 -0
- package/lib/api/endpoints/assistants.js +50 -0
- package/lib/api/endpoints/audit.js +14 -0
- package/lib/api/endpoints/auth.js +50 -0
- package/lib/api/endpoints/awareness.js +34 -0
- package/lib/api/endpoints/backup-schedules.js +20 -0
- package/lib/api/endpoints/backups.js +86 -0
- package/lib/api/endpoints/brain.js +140 -0
- package/lib/api/endpoints/budgets.js +62 -0
- package/lib/api/endpoints/calendarq.js +71 -0
- package/lib/api/endpoints/catalog.js +14 -0
- package/lib/api/endpoints/chat.js +54 -0
- package/lib/api/endpoints/consent.js +26 -0
- package/lib/api/endpoints/contactsq.js +66 -0
- package/lib/api/endpoints/conversations.js +32 -0
- package/lib/api/endpoints/custom-roles.js +92 -0
- package/lib/api/endpoints/divisions.js +56 -0
- package/lib/api/endpoints/domains.js +38 -0
- package/lib/api/endpoints/engagement.js +231 -0
- package/lib/api/endpoints/engine.js +11 -0
- package/lib/api/endpoints/entity.js +50 -0
- package/lib/api/endpoints/executions.js +32 -0
- package/lib/api/endpoints/file.js +20 -0
- package/lib/api/endpoints/files.js +26 -0
- package/lib/api/endpoints/github.js +21 -0
- package/lib/api/endpoints/governance.js +43 -0
- package/lib/api/endpoints/hat.js +36 -0
- package/lib/api/endpoints/inbox.js +32 -0
- package/lib/api/endpoints/inboxq.js +120 -0
- package/lib/api/endpoints/index.js +228 -0
- package/lib/api/endpoints/invites.js +19 -0
- package/lib/api/endpoints/llm-policy.js +43 -0
- package/lib/api/endpoints/lucid.js +89 -0
- package/lib/api/endpoints/members.js +32 -0
- package/lib/api/endpoints/memory.js +75 -0
- package/lib/api/endpoints/mfa.js +32 -0
- package/lib/api/endpoints/mon.js +178 -0
- package/lib/api/endpoints/notesq.js +43 -0
- package/lib/api/endpoints/objectives.js +62 -0
- package/lib/api/endpoints/orgs.js +38 -0
- package/lib/api/endpoints/plugins.js +134 -0
- package/lib/api/endpoints/powers.js +75 -0
- package/lib/api/endpoints/prompts.js +69 -0
- package/lib/api/endpoints/qlarity.js +122 -0
- package/lib/api/endpoints/quoxbox.js +55 -0
- package/lib/api/endpoints/quoxchat.js +54 -0
- package/lib/api/endpoints/quoxflow.js +32 -0
- package/lib/api/endpoints/quoxplan.js +114 -0
- package/lib/api/endpoints/quoxseo.js +108 -0
- package/lib/api/endpoints/reach.js +47 -0
- package/lib/api/endpoints/runs.js +38 -0
- package/lib/api/endpoints/safety.js +46 -0
- package/lib/api/endpoints/scim.js +46 -0
- package/lib/api/endpoints/scopes.js +14 -0
- package/lib/api/endpoints/security.js +69 -0
- package/lib/api/endpoints/setup.js +90 -0
- package/lib/api/endpoints/skills.js +180 -0
- package/lib/api/endpoints/sso.js +67 -0
- package/lib/api/endpoints/tags.js +32 -0
- package/lib/api/endpoints/teams.js +56 -0
- package/lib/api/endpoints/telephony.js +46 -0
- package/lib/api/endpoints/tg.js +56 -0
- package/lib/api/endpoints/tools.js +50 -0
- package/lib/api/endpoints/traces.js +157 -0
- package/lib/api/endpoints/triggers.js +70 -0
- package/lib/api/endpoints/user-library.js +41 -0
- package/lib/api/endpoints/users.js +20 -0
- package/lib/api/endpoints/v1.js +499 -0
- package/lib/api/endpoints/vault.js +192 -0
- package/lib/api/endpoints/voice.js +22 -0
- package/lib/api/endpoints/volt.js +84 -0
- package/lib/api/endpoints/ward.js +40 -0
- package/lib/api/endpoints/webhooks.js +46 -0
- package/lib/api/endpoints/workflows.js +162 -0
- package/lib/api/endpoints/x.js +24 -0
- package/lib/api/endpoints.js +15 -0
- package/lib/commands/admin.js +359 -0
- package/lib/commands/aee.js +99 -0
- package/lib/commands/agent.js +201 -0
- package/lib/commands/agentAttach.js +1440 -0
- package/lib/commands/approvals.js +47 -0
- package/lib/commands/assistant.js +114 -0
- package/lib/commands/auth.js +458 -0
- package/lib/commands/awareness.js +88 -0
- package/lib/commands/backup.js +373 -0
- package/lib/commands/brain.js +994 -0
- package/lib/commands/budget.js +241 -0
- package/lib/commands/build.js +156 -0
- package/lib/commands/calendar.js +216 -0
- package/lib/commands/capabilities.js +34 -0
- package/lib/commands/chat.js +240 -0
- package/lib/commands/claude.js +354 -0
- package/lib/commands/config-cmd.js +106 -0
- package/lib/commands/consent.js +84 -0
- package/lib/commands/contacts.js +967 -0
- package/lib/commands/contactsSelftest.js +132 -0
- package/lib/commands/conversation.js +74 -0
- package/lib/commands/department.js +75 -0
- package/lib/commands/dev.js +620 -0
- package/lib/commands/devResident.js +314 -0
- package/lib/commands/devSupervisor.js +1050 -0
- package/lib/commands/discord.js +96 -0
- package/lib/commands/doctor.js +181 -0
- package/lib/commands/domain.js +114 -0
- package/lib/commands/done.js +277 -0
- package/lib/commands/engagement.js +586 -0
- package/lib/commands/entity.js +119 -0
- package/lib/commands/facebook.js +212 -0
- package/lib/commands/file.js +128 -0
- package/lib/commands/fleet.js +882 -0
- package/lib/commands/governance.js +638 -0
- package/lib/commands/gripe.js +324 -0
- package/lib/commands/hat.js +205 -0
- package/lib/commands/helpers.js +79 -0
- package/lib/commands/inbox.js +122 -0
- package/lib/commands/instagram.js +264 -0
- package/lib/commands/integration.js +100 -0
- package/lib/commands/inventory.js +115 -0
- package/lib/commands/invites.js +45 -0
- package/lib/commands/licence.js +280 -0
- package/lib/commands/linkedin.js +178 -0
- package/lib/commands/llm-policy.js +117 -0
- package/lib/commands/llm.js +45 -0
- package/lib/commands/lucid.js +591 -0
- package/lib/commands/mail.js +531 -0
- package/lib/commands/memory.js +284 -0
- package/lib/commands/mon.js +514 -0
- package/lib/commands/monitoring.js +86 -0
- package/lib/commands/motion.js +22 -0
- package/lib/commands/notes.js +1246 -0
- package/lib/commands/notesBatch.js +160 -0
- package/lib/commands/notesBridges.js +56 -0
- package/lib/commands/notesLinks.js +60 -0
- package/lib/commands/notification.js +57 -0
- package/lib/commands/nucleus.js +92 -0
- package/lib/commands/objective.js +229 -0
- package/lib/commands/onboard.js +328 -0
- package/lib/commands/open.js +56 -0
- package/lib/commands/org.js +266 -0
- package/lib/commands/plan.js +906 -0
- package/lib/commands/plugin-doctor.js +1091 -0
- package/lib/commands/plugin.js +539 -0
- package/lib/commands/policy.js +78 -0
- package/lib/commands/powers.js +296 -0
- package/lib/commands/prompt.js +350 -0
- package/lib/commands/qlarity.js +690 -0
- package/lib/commands/quoxbox.js +270 -0
- package/lib/commands/quoxchat.js +266 -0
- package/lib/commands/quoxseo.js +340 -0
- package/lib/commands/reach.js +158 -0
- package/lib/commands/role.js +286 -0
- package/lib/commands/run.js +132 -0
- package/lib/commands/safety.js +107 -0
- package/lib/commands/scim.js +88 -0
- package/lib/commands/scope.js +58 -0
- package/lib/commands/security.js +157 -0
- package/lib/commands/service.js +89 -0
- package/lib/commands/settings.js +99 -0
- package/lib/commands/setup.js +543 -0
- package/lib/commands/skillManifest.js +113 -0
- package/lib/commands/skillRightsFormat.js +38 -0
- package/lib/commands/skills.js +644 -0
- package/lib/commands/slot.js +1198 -0
- package/lib/commands/sso.js +229 -0
- package/lib/commands/status.js +137 -0
- package/lib/commands/tag.js +91 -0
- package/lib/commands/team.js +140 -0
- package/lib/commands/tg.js +240 -0
- package/lib/commands/tiktok.js +218 -0
- package/lib/commands/tool.js +216 -0
- package/lib/commands/trace.js +525 -0
- package/lib/commands/trigger.js +128 -0
- package/lib/commands/vault.js +1424 -0
- package/lib/commands/voice.js +182 -0
- package/lib/commands/volt.js +528 -0
- package/lib/commands/watch.js +115 -0
- package/lib/commands/webhook.js +137 -0
- package/lib/commands/workflow.js +503 -0
- package/lib/commands/x.js +439 -0
- package/lib/core/actingAs.js +136 -0
- package/lib/core/aee.js +53 -0
- package/lib/core/agentAlias.js +163 -0
- package/lib/core/approvalContext.js +61 -0
- package/lib/core/auth.js +350 -0
- package/lib/core/banner.js +143 -0
- package/lib/core/brainPrime.js +90 -0
- package/lib/core/callerSource.js +129 -0
- package/lib/core/claudeLs.js +94 -0
- package/lib/core/client.js +584 -0
- package/lib/core/completions.js +163 -0
- package/lib/core/config.js +551 -0
- package/lib/core/confirm.js +37 -0
- package/lib/core/deeplinks.js +76 -0
- package/lib/core/doneEngineFetch.js +219 -0
- package/lib/core/errors.js +132 -0
- package/lib/core/global-flags.js +90 -0
- package/lib/core/governedSession.js +402 -0
- package/lib/core/lazyCommands.js +119 -0
- package/lib/core/liveSessionsReport.js +112 -0
- package/lib/core/org-override.js +52 -0
- package/lib/core/output.js +239 -0
- package/lib/core/prodEnv.js +7 -0
- package/lib/core/quoxglyph.js +158 -0
- package/lib/core/residentAlarm.js +282 -0
- package/lib/core/residentAuth.js +243 -0
- package/lib/core/residentPump.js +229 -0
- package/lib/core/residentSeed.js +104 -0
- package/lib/core/sessionHeartbeat.js +192 -0
- package/lib/core/sessionsOverview.js +160 -0
- package/lib/core/slotKeeper.js +282 -0
- package/lib/core/supervisorEngine.js +864 -0
- package/lib/core/systemdInstaller.js +256 -0
- package/lib/core/version.js +87 -0
- package/lib/core/websiteSession.js +148 -0
- package/lib/lucid/LucidIntro.jsx +105 -0
- package/lib/lucid/blind.js +71 -0
- package/lib/lucid/deck.js +32 -0
- package/lib/lucid/fmt.js +139 -0
- package/lib/lucid/sitrep.js +173 -0
- package/lib/plugins/loader.js +93 -0
- package/lib/quoxon/activity.js +104 -0
- package/lib/quoxon/api.js +473 -0
- package/lib/quoxon/app.js +1262 -0
- package/lib/quoxon/capabilities.js +75 -0
- package/lib/quoxon/commands.js +154 -0
- package/lib/quoxon/connect.js +467 -0
- package/lib/quoxon/connectCard.js +28 -0
- package/lib/quoxon/context.js +68 -0
- package/lib/quoxon/copy.js +33 -0
- package/lib/quoxon/demo.fixtures.js +24 -0
- package/lib/quoxon/demo.js +35 -0
- package/lib/quoxon/egg.js +36 -0
- package/lib/quoxon/events.js +458 -0
- package/lib/quoxon/gallery.js +223 -0
- package/lib/quoxon/indicator.js +193 -0
- package/lib/quoxon/intent.js +41 -0
- package/lib/quoxon/keys.js +121 -0
- package/lib/quoxon/main.js +244 -0
- package/lib/quoxon/markdown.js +317 -0
- package/lib/quoxon/model.js +205 -0
- package/lib/quoxon/motion/clock.js +69 -0
- package/lib/quoxon/motion/color.js +35 -0
- package/lib/quoxon/motion/frame.js +14 -0
- package/lib/quoxon/motion/reveal.js +54 -0
- package/lib/quoxon/motion/ring.js +79 -0
- package/lib/quoxon/objects/agent.js +61 -0
- package/lib/quoxon/objects/crystal.js +100 -0
- package/lib/quoxon/objects/fetch.js +170 -0
- package/lib/quoxon/objects/index.js +85 -0
- package/lib/quoxon/objects/ladder.js +71 -0
- package/lib/quoxon/objects/memory.js +44 -0
- package/lib/quoxon/objects/model.js +118 -0
- package/lib/quoxon/objects/proof.js +110 -0
- package/lib/quoxon/objects/skill.js +105 -0
- package/lib/quoxon/objects/view.js +76 -0
- package/lib/quoxon/objects/workflow.js +119 -0
- package/lib/quoxon/observers.js +42 -0
- package/lib/quoxon/onboarding/copy.js +235 -0
- package/lib/quoxon/onboarding/credential.js +95 -0
- package/lib/quoxon/onboarding/detect.js +22 -0
- package/lib/quoxon/onboarding/finish.js +119 -0
- package/lib/quoxon/onboarding/index.js +67 -0
- package/lib/quoxon/onboarding/installRunner.js +225 -0
- package/lib/quoxon/onboarding/intent.js +53 -0
- package/lib/quoxon/onboarding/journey.js +73 -0
- package/lib/quoxon/onboarding/preflight.js +142 -0
- package/lib/quoxon/onboarding/runner.js +58 -0
- package/lib/quoxon/onboarding/view.js +625 -0
- package/lib/quoxon/print.js +94 -0
- package/lib/quoxon/qart.js +379 -0
- package/lib/quoxon/qglyph.js +143 -0
- package/lib/quoxon/qhello.js +58 -0
- package/lib/quoxon/qmotion.js +111 -0
- package/lib/quoxon/render.js +648 -0
- package/lib/quoxon/roster.js +221 -0
- package/lib/quoxon/route.js +38 -0
- package/lib/quoxon/rules.js +130 -0
- package/lib/quoxon/session.js +68 -0
- package/lib/quoxon/skills.js +30 -0
- package/lib/quoxon/slash/compact.js +90 -0
- package/lib/quoxon/slash/ledger.js +83 -0
- package/lib/quoxon/slash/listView.js +28 -0
- package/lib/quoxon/slash/palette.js +104 -0
- package/lib/quoxon/slash/paletteController.js +59 -0
- package/lib/quoxon/slash/panels.js +101 -0
- package/lib/quoxon/slash/picker.js +38 -0
- package/lib/quoxon/slash/restart.js +144 -0
- package/lib/quoxon/slash/resume.js +59 -0
- package/lib/quoxon/slash/settings.js +50 -0
- package/lib/quoxon/sound/index.js +186 -0
- package/lib/quoxon/sound/mapping.js +119 -0
- package/lib/quoxon/sound/output.js +52 -0
- package/lib/quoxon/sound/synth.js +122 -0
- package/lib/quoxon/term.js +106 -0
- package/lib/quoxon/textBuffer.js +242 -0
- package/lib/quoxon/theme.js +253 -0
- package/lib/quoxon/transcript.js +150 -0
- package/lib/quoxon/web/qface.js +81 -0
- package/lib/quoxon/welcome.js +52 -0
- package/lib/tui/app.jsx +289 -0
- package/lib/tui/components/Breadcrumb.jsx +42 -0
- package/lib/tui/components/ChatInput.jsx +68 -0
- package/lib/tui/components/Confirm.jsx +32 -0
- package/lib/tui/components/DataTable.jsx +270 -0
- package/lib/tui/components/DetailPanel.jsx +241 -0
- package/lib/tui/components/HelpOverlay.jsx +67 -0
- package/lib/tui/components/Layout.jsx +129 -0
- package/lib/tui/components/Palette.jsx +141 -0
- package/lib/tui/components/SearchInput.jsx +125 -0
- package/lib/tui/components/Sparkline.jsx +55 -0
- package/lib/tui/components/StatusBar.jsx +133 -0
- package/lib/tui/components/Toast.jsx +52 -0
- package/lib/tui/hooks/useApi.js +44 -0
- package/lib/tui/hooks/useHealth.js +90 -0
- package/lib/tui/hooks/usePolledApi.js +86 -0
- package/lib/tui/hooks/useTerminalSize.js +46 -0
- package/lib/tui/index.js +56 -0
- package/lib/tui/intro/Intro.jsx +332 -0
- package/lib/tui/jsx-loader.js +32 -0
- package/lib/tui/keys.js +87 -0
- package/lib/tui/registry.js +49 -0
- package/lib/tui/theme.js +103 -0
- package/lib/tui/views/AdminView.jsx +398 -0
- package/lib/tui/views/AgentView.jsx +204 -0
- package/lib/tui/views/BackupView.jsx +225 -0
- package/lib/tui/views/BrainView.jsx +223 -0
- package/lib/tui/views/ChatView.jsx +468 -0
- package/lib/tui/views/FilesView.jsx +218 -0
- package/lib/tui/views/FleetView.jsx +234 -0
- package/lib/tui/views/GovernanceView.jsx +453 -0
- package/lib/tui/views/HealthView.jsx +216 -0
- package/lib/tui/views/InboxView.jsx +228 -0
- package/lib/tui/views/LucidDeck.jsx +727 -0
- package/lib/tui/views/MainMenu.jsx +123 -0
- package/lib/tui/views/MemoryView.jsx +377 -0
- package/lib/tui/views/ObjectivesView.jsx +230 -0
- package/lib/tui/views/OrgView.jsx +229 -0
- package/lib/tui/views/ToolView.jsx +221 -0
- package/lib/tui/views/VaultView.jsx +422 -0
- package/lib/tui/views/WorkflowView.jsx +329 -0
- package/package.json +72 -4
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook commands: org webhook subscribers (team_event fan-out targets).
|
|
3
|
+
*
|
|
4
|
+
* quox webhook list|create|rotate|delete
|
|
5
|
+
*
|
|
6
|
+
* Backend: services/auth/routes/webhooks.js, mounted at plain `/api`
|
|
7
|
+
* (org-scoped IN the URL: /api/orgs/:orgId/webhooks...). Enterprise-tier +
|
|
8
|
+
* ORG_MANAGE_SETTINGS gated — expect 402/403 on non-enterprise orgs.
|
|
9
|
+
*
|
|
10
|
+
* The secret is returned ONLY on create and rotate (plaintext, once) — it
|
|
11
|
+
* is never retrievable again after that response, by design (HMAC signing
|
|
12
|
+
* secret for payload verification). We print it prominently and warn the
|
|
13
|
+
* user to record it now.
|
|
14
|
+
*
|
|
15
|
+
* Valid `events` values are the frozen TEAM_EVENT_TYPES set
|
|
16
|
+
* (services/auth/utils/teamEvents.js): team.created, team.archived,
|
|
17
|
+
* team.member.invited, team.member.joined, team.member.left,
|
|
18
|
+
* team.member.role_changed. Not enumerated client-side — the server
|
|
19
|
+
* validates and returns `invalid_webhook` on an unknown type.
|
|
20
|
+
*
|
|
21
|
+
* Distinct from `quox trigger webhooks` (endpoints/triggers.js), which
|
|
22
|
+
* lists INBOUND workflow-trigger webhook paths on quoxflow, not these
|
|
23
|
+
* outbound org subscriptions.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { formatOutput, formatDetail, printSuccess, printWarning, printInfo } from '../core/output.js';
|
|
27
|
+
import { resolveOrgId } from './helpers.js';
|
|
28
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
29
|
+
|
|
30
|
+
function printSecret(secret) {
|
|
31
|
+
printWarning('Record this secret now — it will not be shown again:');
|
|
32
|
+
console.log(` ${secret}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function registerWebhookCommands(program, ctx) {
|
|
36
|
+
const { client, auth } = ctx;
|
|
37
|
+
|
|
38
|
+
const webhook = program
|
|
39
|
+
.command('webhook')
|
|
40
|
+
.description('Manage org webhook subscribers (enterprise — team_event fan-out)');
|
|
41
|
+
|
|
42
|
+
// ── webhook list ─────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
webhook
|
|
45
|
+
.command('list')
|
|
46
|
+
.description('List org webhook subscriptions')
|
|
47
|
+
.action(async () => {
|
|
48
|
+
auth.requireAuth();
|
|
49
|
+
const orgId = resolveOrgId(auth, program);
|
|
50
|
+
const data = await client.get(resolveEndpoint('webhooks.list', { orgId }).path, {
|
|
51
|
+
service: resolveEndpoint('webhooks.list', { orgId }).service,
|
|
52
|
+
});
|
|
53
|
+
const webhooks = data.webhooks || data || [];
|
|
54
|
+
|
|
55
|
+
formatOutput({
|
|
56
|
+
columns: ['id', 'url', 'events', 'enabled', 'created_at'],
|
|
57
|
+
headers: ['ID', 'URL', 'Events', 'Enabled', 'Created'],
|
|
58
|
+
rows: webhooks.map(w => ({
|
|
59
|
+
...w,
|
|
60
|
+
events: Array.isArray(w.events) ? w.events.join(', ') : w.events,
|
|
61
|
+
})),
|
|
62
|
+
}, { format: program.opts().output, emptyMessage: 'No webhooks configured.' });
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ── webhook create ──────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
webhook
|
|
68
|
+
.command('create')
|
|
69
|
+
.description('Create an org webhook subscription')
|
|
70
|
+
.requiredOption('--url <url>', 'https:// endpoint to receive events')
|
|
71
|
+
.requiredOption('--events <events>', 'comma-separated team_event types (e.g. team.created,team.member.joined)')
|
|
72
|
+
.action(async (options) => {
|
|
73
|
+
auth.requireAuth();
|
|
74
|
+
const orgId = resolveOrgId(auth, program);
|
|
75
|
+
const events = options.events.split(',').map(e => e.trim()).filter(Boolean);
|
|
76
|
+
|
|
77
|
+
const data = await client.post(resolveEndpoint('webhooks.create', { orgId }).path, {
|
|
78
|
+
service: resolveEndpoint('webhooks.create', { orgId }).service,
|
|
79
|
+
body: { url: options.url, events },
|
|
80
|
+
});
|
|
81
|
+
const created = data.webhook || data;
|
|
82
|
+
printSuccess(`Webhook created for ${options.url}.`);
|
|
83
|
+
formatDetail(created, {
|
|
84
|
+
format: program.opts().output,
|
|
85
|
+
fields: ['id', 'url', 'events', 'enabled', 'created_at'],
|
|
86
|
+
labels: { id: 'ID', url: 'URL', events: 'Events', enabled: 'Enabled', created_at: 'Created' },
|
|
87
|
+
});
|
|
88
|
+
if (created.secret) printSecret(created.secret);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// ── webhook rotate ──────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
webhook
|
|
94
|
+
.command('rotate')
|
|
95
|
+
.description('Rotate a webhook\'s signing secret')
|
|
96
|
+
.argument('<id>', 'webhook ID')
|
|
97
|
+
.action(async (id) => {
|
|
98
|
+
auth.requireAuth();
|
|
99
|
+
const orgId = resolveOrgId(auth, program);
|
|
100
|
+
const data = await client.post(resolveEndpoint('webhooks.rotate', { orgId, id }).path, {
|
|
101
|
+
service: resolveEndpoint('webhooks.rotate', { orgId, id }).service,
|
|
102
|
+
});
|
|
103
|
+
printSuccess(`Webhook ${id} secret rotated.`);
|
|
104
|
+
if (data.secret) printSecret(data.secret);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// ── webhook delete ──────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
webhook
|
|
110
|
+
.command('delete')
|
|
111
|
+
.description('Delete an org webhook subscription')
|
|
112
|
+
.argument('<id>', 'webhook ID')
|
|
113
|
+
.option('--force', 'skip confirmation')
|
|
114
|
+
.action(async (id, options) => {
|
|
115
|
+
auth.requireAuth();
|
|
116
|
+
const orgId = resolveOrgId(auth, program);
|
|
117
|
+
|
|
118
|
+
if (!options.force && !program.opts().yes) {
|
|
119
|
+
const { default: inquirer } = await import('inquirer');
|
|
120
|
+
const ans = await inquirer.prompt([{
|
|
121
|
+
type: 'confirm',
|
|
122
|
+
name: 'confirm',
|
|
123
|
+
message: `Delete webhook ${id}? This cannot be undone.`,
|
|
124
|
+
default: false,
|
|
125
|
+
}]);
|
|
126
|
+
if (!ans.confirm) {
|
|
127
|
+
printInfo('Cancelled.');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await client.delete(resolveEndpoint('webhooks.delete', { orgId, id }).path, {
|
|
133
|
+
service: resolveEndpoint('webhooks.delete', { orgId, id }).service,
|
|
134
|
+
});
|
|
135
|
+
printSuccess('Webhook deleted.');
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow commands: CRUD, run, steps, versions, declarative apply/validate/import (WP2.5)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { extname } from 'node:path';
|
|
7
|
+
import yaml from 'js-yaml';
|
|
8
|
+
import { formatOutput, formatDetail, printWarning, printInfo } from '../core/output.js';
|
|
9
|
+
import { events, printEvent, line as glyphLine } from '../core/quoxglyph.js';
|
|
10
|
+
import { resolveOrgId, parseJSON } from './helpers.js';
|
|
11
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
12
|
+
import { confirmDestructive } from '../core/confirm.js';
|
|
13
|
+
|
|
14
|
+
// Top-level Workflow fields compared for `workflow apply`'s diff-preview
|
|
15
|
+
// (mirrors core/workflow.ts's WorkflowSchema field set, minus `id`).
|
|
16
|
+
const APPLY_DIFF_FIELDS = ['name', 'description', 'notes', 'version', 'nodes', 'edges', 'variables', 'credentials', 'settings', 'metadata'];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read a declarative workflow/template file (.yaml/.yml/.json) and parse it
|
|
20
|
+
* to a plain object. Falls back to trying both parsers when the extension
|
|
21
|
+
* is unrecognized (YAML is a superset of JSON, so this also covers a
|
|
22
|
+
* misnamed .json-shaped file).
|
|
23
|
+
*/
|
|
24
|
+
function readDeclarativeFile(filePath) {
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = readFileSync(filePath, 'utf-8');
|
|
28
|
+
} catch (err) {
|
|
29
|
+
throw new Error(`Could not read ${filePath}: ${err.message}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const ext = extname(filePath).toLowerCase();
|
|
33
|
+
try {
|
|
34
|
+
if (ext === '.json') return JSON.parse(raw);
|
|
35
|
+
if (ext === '.yaml' || ext === '.yml') return yaml.load(raw);
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(raw);
|
|
38
|
+
} catch {
|
|
39
|
+
return yaml.load(raw);
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
throw new Error(`Could not parse ${filePath} (expected YAML or JSON): ${err.message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Short human-readable summary of a field value for the diff-preview. */
|
|
47
|
+
function summarizeValue(value) {
|
|
48
|
+
if (value === undefined) return '(unset)';
|
|
49
|
+
if (Array.isArray(value)) return `${value.length} item(s)`;
|
|
50
|
+
if (value !== null && typeof value === 'object') return `${Object.keys(value).length} key(s)`;
|
|
51
|
+
return JSON.stringify(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build a field-level diff between the currently-stored workflow (from
|
|
56
|
+
* `workflows.applyGet`) and the local file, restricted to fields present in
|
|
57
|
+
* the local file — used by `workflow apply` to preview a PUT before firing it.
|
|
58
|
+
*/
|
|
59
|
+
function buildApplyDiff(existing, incoming) {
|
|
60
|
+
const existingDef = existing.definition || existing;
|
|
61
|
+
const changes = [];
|
|
62
|
+
for (const key of APPLY_DIFF_FIELDS) {
|
|
63
|
+
if (!(key in incoming)) continue;
|
|
64
|
+
const before = existingDef[key];
|
|
65
|
+
const after = incoming[key];
|
|
66
|
+
if (JSON.stringify(before) === JSON.stringify(after)) continue;
|
|
67
|
+
changes.push({ key, before, after });
|
|
68
|
+
}
|
|
69
|
+
return changes;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function printApplyDiff(workflowId, changes) {
|
|
73
|
+
console.log(`\nChanges to workflow ${workflowId}:`);
|
|
74
|
+
for (const { key, before, after } of changes) {
|
|
75
|
+
console.log(` ${key}: ${summarizeValue(before)} -> ${summarizeValue(after)}`);
|
|
76
|
+
}
|
|
77
|
+
console.log('');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function registerWorkflowCommands(program, ctx) {
|
|
81
|
+
const { client, auth } = ctx;
|
|
82
|
+
|
|
83
|
+
const workflow = program
|
|
84
|
+
.command('workflow')
|
|
85
|
+
.description('Manage workflows');
|
|
86
|
+
|
|
87
|
+
// fleet #465: workflows live in two stores. The engine (quoxflow) is the
|
|
88
|
+
// WRITE path for `workflow apply`/`activate`, but the auth-service store
|
|
89
|
+
// is a completely separate model quoxflow never writes to. `list` and
|
|
90
|
+
// `get` below read BOTH (or try engine-first with a fallback) and label
|
|
91
|
+
// which store answered, instead of silently missing engine-created
|
|
92
|
+
// workflows the way the auth-only read used to.
|
|
93
|
+
|
|
94
|
+
workflow
|
|
95
|
+
.command('list')
|
|
96
|
+
.description('List workflows')
|
|
97
|
+
.option('--status <status>', 'filter by status')
|
|
98
|
+
.action(async (options) => {
|
|
99
|
+
auth.requireAuth();
|
|
100
|
+
const orgId = resolveOrgId(auth, program);
|
|
101
|
+
const query = {};
|
|
102
|
+
if (options.status) query.status = options.status;
|
|
103
|
+
|
|
104
|
+
const rows = [];
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const ep = resolveEndpoint('workflows.applyList');
|
|
108
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
109
|
+
const items = data.data || data.items || data.workflows || (Array.isArray(data) ? data : []);
|
|
110
|
+
for (const item of items) rows.push({ ...item, source: 'engine' });
|
|
111
|
+
} catch (err) {
|
|
112
|
+
printWarning(`Engine (quoxflow) workflow list failed: ${err.message}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const ep = resolveEndpoint('workflows.list', { orgId });
|
|
117
|
+
const data = await client.get(ep.path, { service: ep.service, query });
|
|
118
|
+
const items = data.items || data.workflows || (Array.isArray(data) ? data : []);
|
|
119
|
+
for (const item of items) rows.push({ ...item, source: 'auth' });
|
|
120
|
+
} catch (err) {
|
|
121
|
+
printWarning(`Auth-service workflow list failed: ${err.message}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
formatOutput({
|
|
125
|
+
columns: ['id', 'name', 'status', 'source', 'step_count', 'last_run', 'created_at'],
|
|
126
|
+
headers: ['ID', 'Name', 'Status', 'Source', 'Steps', 'Last Run', 'Created'],
|
|
127
|
+
rows,
|
|
128
|
+
}, { format: program.opts().output });
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
workflow
|
|
132
|
+
.command('get')
|
|
133
|
+
.description('Get workflow details')
|
|
134
|
+
.argument('<workflowId>', 'workflow ID')
|
|
135
|
+
.action(async (workflowId) => {
|
|
136
|
+
auth.requireAuth();
|
|
137
|
+
const orgId = resolveOrgId(auth, program);
|
|
138
|
+
|
|
139
|
+
let data, source;
|
|
140
|
+
try {
|
|
141
|
+
const ep = resolveEndpoint('workflows.applyGet', { workflowId });
|
|
142
|
+
const res = await client.get(ep.path, { service: ep.service });
|
|
143
|
+
data = res.data || res;
|
|
144
|
+
source = 'engine';
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (err.status !== 404) throw err;
|
|
147
|
+
try {
|
|
148
|
+
const ep = resolveEndpoint('workflows.get', { orgId, workflowId });
|
|
149
|
+
const res = await client.get(ep.path, { service: ep.service });
|
|
150
|
+
data = res.workflow || res;
|
|
151
|
+
source = 'auth';
|
|
152
|
+
} catch (err2) {
|
|
153
|
+
if (err2.status !== 404) throw err2;
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Workflow "${workflowId}" was not found in either the engine (quoxflow) or auth-service workflow store.`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
formatDetail({ ...data, source }, { format: program.opts().output, title: data.name });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
workflow
|
|
164
|
+
.command('create')
|
|
165
|
+
.description('Create a new workflow')
|
|
166
|
+
.requiredOption('--name <name>', 'workflow name')
|
|
167
|
+
.option('--description <desc>', 'description')
|
|
168
|
+
.option('--steps <json>', 'workflow steps as JSON')
|
|
169
|
+
.action(async (options) => {
|
|
170
|
+
auth.requireAuth();
|
|
171
|
+
const orgId = resolveOrgId(auth, program);
|
|
172
|
+
const body = { name: options.name };
|
|
173
|
+
if (options.description) body.description = options.description;
|
|
174
|
+
if (options.steps) body.steps = parseJSON(options.steps, '--steps');
|
|
175
|
+
await client.post(resolveEndpoint('workflows.create', { orgId }).path, { service: resolveEndpoint('workflows.create', { orgId }).service, body });
|
|
176
|
+
printEvent(events.created('quox', 'workflow', options.name));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
workflow
|
|
180
|
+
.command('update')
|
|
181
|
+
.description('Update a workflow')
|
|
182
|
+
.argument('<workflowId>', 'workflow ID')
|
|
183
|
+
.option('--name <name>', 'new name')
|
|
184
|
+
.option('--description <desc>', 'new description')
|
|
185
|
+
.option('--status <status>', 'new status')
|
|
186
|
+
.action(async (workflowId, options) => {
|
|
187
|
+
auth.requireAuth();
|
|
188
|
+
const orgId = resolveOrgId(auth, program);
|
|
189
|
+
const body = {};
|
|
190
|
+
if (options.name) body.name = options.name;
|
|
191
|
+
if (options.description) body.description = options.description;
|
|
192
|
+
if (options.status) body.status = options.status;
|
|
193
|
+
await client.patch(resolveEndpoint('workflows.update', { orgId, workflowId }).path, { service: resolveEndpoint('workflows.update', { orgId, workflowId }).service, body });
|
|
194
|
+
printEvent(events.updated('quox', 'workflow', workflowId));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
workflow
|
|
198
|
+
.command('delete')
|
|
199
|
+
.description('Delete a workflow')
|
|
200
|
+
.argument('<workflowId>', 'workflow ID')
|
|
201
|
+
.action(async (workflowId) => {
|
|
202
|
+
auth.requireAuth();
|
|
203
|
+
const orgId = resolveOrgId(auth, program);
|
|
204
|
+
await client.delete(resolveEndpoint('workflows.delete', { orgId, workflowId }).path, { service: resolveEndpoint('workflows.delete', { orgId, workflowId }).service });
|
|
205
|
+
printEvent(glyphLine('quox', 'workflow deleted', { result: workflowId, status: 'ok' }));
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
workflow
|
|
209
|
+
.command('run')
|
|
210
|
+
.description('Execute a workflow (must be published/active — see `workflow publish`)')
|
|
211
|
+
.argument('<workflowId>', 'workflow ID')
|
|
212
|
+
.option('--input <json>', 'input data as JSON (passed as run context)')
|
|
213
|
+
.action(async (workflowId, options) => {
|
|
214
|
+
auth.requireAuth();
|
|
215
|
+
const orgId = resolveOrgId(auth, program);
|
|
216
|
+
const body = { workflow_id: workflowId };
|
|
217
|
+
if (options.input) body.context = parseJSON(options.input, '--input');
|
|
218
|
+
try {
|
|
219
|
+
const data = await client.post(resolveEndpoint('runs.create', { orgId }).path, { service: resolveEndpoint('runs.create', { orgId }).service, body });
|
|
220
|
+
printEvent(glyphLine('quox', 'workflow run', { result: `${workflowId} -> run ${data.run?.id || data.run_id || data.id || 'ok'}`, status: 'ok' }));
|
|
221
|
+
} catch (err) {
|
|
222
|
+
if (err.status === 404) {
|
|
223
|
+
// quoxflow#7: the auth runs route only knows auth-store workflows. If
|
|
224
|
+
// the engine store has it, execute it there instead.
|
|
225
|
+
let engineExists = false;
|
|
226
|
+
try {
|
|
227
|
+
const getEp = resolveEndpoint('workflows.applyGet', { workflowId });
|
|
228
|
+
await client.get(getEp.path, { service: getEp.service });
|
|
229
|
+
engineExists = true;
|
|
230
|
+
} catch (engineErr) {
|
|
231
|
+
if (engineErr.status !== 404) throw engineErr;
|
|
232
|
+
}
|
|
233
|
+
if (engineExists) {
|
|
234
|
+
const exEp = resolveEndpoint('workflows.applyExecute', { workflowId });
|
|
235
|
+
const run = await client.post(exEp.path, { service: exEp.service, body: body.context || {} });
|
|
236
|
+
const runId = run.executionId || run.execution_id || run.runId || run.run_id || run.id || run.execution?.id || 'ok';
|
|
237
|
+
const st = run.status || run.execution?.status;
|
|
238
|
+
printEvent(glyphLine('quox', 'workflow run', { result: `${workflowId} (quoxflow) -> run ${runId}${st ? ` [${st}]` : ''}`, status: 'ok' }));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
throw new Error(
|
|
242
|
+
`Workflow not found or not active. Only published workflows can be run — try \`quox workflow publish ${workflowId}\` first.`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
workflow
|
|
250
|
+
.command('publish')
|
|
251
|
+
.description('Publish a workflow')
|
|
252
|
+
.argument('<workflowId>', 'workflow ID')
|
|
253
|
+
.action(async (workflowId) => {
|
|
254
|
+
auth.requireAuth();
|
|
255
|
+
const orgId = resolveOrgId(auth, program);
|
|
256
|
+
try {
|
|
257
|
+
const ep = resolveEndpoint('workflows.publish', { orgId, workflowId });
|
|
258
|
+
await client.post(ep.path, { service: ep.service });
|
|
259
|
+
printEvent(glyphLine('quox', 'workflow published', { result: workflowId, status: 'ok' }));
|
|
260
|
+
} catch (err) {
|
|
261
|
+
if (err.status !== 404) throw err;
|
|
262
|
+
|
|
263
|
+
// fleet #465: publish operates on the auth-service model only. A
|
|
264
|
+
// workflow that exists solely in the engine (e.g. via `workflow
|
|
265
|
+
// apply`) 404s here even though it exists — check the engine before
|
|
266
|
+
// reporting a bare "not found".
|
|
267
|
+
let engineExists = false;
|
|
268
|
+
try {
|
|
269
|
+
const getEp = resolveEndpoint('workflows.applyGet', { workflowId });
|
|
270
|
+
await client.get(getEp.path, { service: getEp.service });
|
|
271
|
+
engineExists = true;
|
|
272
|
+
} catch (engineErr) {
|
|
273
|
+
if (engineErr.status !== 404) throw engineErr;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (engineExists) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`Workflow "${workflowId}" lives in the engine (quoxflow) store, not the auth-service store that `
|
|
279
|
+
+ `\`workflow publish\` operates on. Use \`quox trigger workflow-activate ${workflowId}\` to make it live — `
|
|
280
|
+
+ '`workflow publish` only applies to workflows created via `workflow create`.'
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
throw new Error(
|
|
285
|
+
`Workflow "${workflowId}" was not found in either the engine (quoxflow) or auth-service workflow store.`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// Steps subcommand
|
|
291
|
+
const steps = workflow
|
|
292
|
+
.command('steps')
|
|
293
|
+
.description('Manage workflow steps');
|
|
294
|
+
|
|
295
|
+
steps
|
|
296
|
+
.command('list')
|
|
297
|
+
.description('List steps of a workflow')
|
|
298
|
+
.argument('<workflowId>', 'workflow ID')
|
|
299
|
+
.action(async (workflowId) => {
|
|
300
|
+
auth.requireAuth();
|
|
301
|
+
const orgId = resolveOrgId(auth, program);
|
|
302
|
+
// No dedicated GET .../steps route exists — steps are a field on the
|
|
303
|
+
// workflow object itself.
|
|
304
|
+
const data = await client.get(resolveEndpoint('workflows.get', { orgId, workflowId }).path, { service: resolveEndpoint('workflows.get', { orgId, workflowId }).service });
|
|
305
|
+
const workflow = data.workflow || data;
|
|
306
|
+
formatOutput({
|
|
307
|
+
// fleet #392: workflow_steps columns are idx/label/step_type/ref_id.
|
|
308
|
+
columns: ['idx', 'label', 'step_type', 'ref_id'],
|
|
309
|
+
headers: ['#', 'Name', 'Type', 'Tool'],
|
|
310
|
+
rows: workflow.steps || [],
|
|
311
|
+
}, { format: program.opts().output });
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
steps
|
|
315
|
+
.command('set')
|
|
316
|
+
.description('Set workflow steps (replaces all)')
|
|
317
|
+
.argument('<workflowId>', 'workflow ID')
|
|
318
|
+
.requiredOption('--steps <json>', 'steps as JSON array')
|
|
319
|
+
.action(async (workflowId, options) => {
|
|
320
|
+
auth.requireAuth();
|
|
321
|
+
const orgId = resolveOrgId(auth, program);
|
|
322
|
+
await client.put(resolveEndpoint('workflows.steps', { orgId, workflowId }).path, {
|
|
323
|
+
service: resolveEndpoint('workflows.steps', { orgId, workflowId }).service,
|
|
324
|
+
body: { steps: parseJSON(options.steps, '--steps') },
|
|
325
|
+
});
|
|
326
|
+
printEvent(events.updated('quox', 'workflow steps', workflowId));
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// ── workflow apply / validate / import-n8n / import-objective (WP2.5) ───────
|
|
330
|
+
//
|
|
331
|
+
// Declarative, file-based create/edit against quoxflow's own workflow
|
|
332
|
+
// engine — DIRECT to quoxflow (service: 'quoxflow'), NOT the auth-service
|
|
333
|
+
// 'workflows.create'/'workflows.update' used by the commands above. See
|
|
334
|
+
// the header comment in lib/api/endpoints/workflows.js for the full
|
|
335
|
+
// access-path + naming-collision rationale.
|
|
336
|
+
|
|
337
|
+
workflow
|
|
338
|
+
.command('apply')
|
|
339
|
+
.description('Create or update a workflow declaratively from a YAML/JSON file (quoxflow-direct: POST if the file has no id, PUT with a diff-preview if it does)')
|
|
340
|
+
.requiredOption('-f, --file <path>', 'path to a workflow definition file (.yaml/.yml/.json)')
|
|
341
|
+
.option('--force', 'skip the diff-preview confirmation on update')
|
|
342
|
+
.action(async (options) => {
|
|
343
|
+
auth.requireAuth();
|
|
344
|
+
const wf = readDeclarativeFile(options.file);
|
|
345
|
+
if (!wf || typeof wf !== 'object') {
|
|
346
|
+
throw new Error(`${options.file} did not parse to a workflow object.`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (wf.id) {
|
|
350
|
+
let existing = null;
|
|
351
|
+
try {
|
|
352
|
+
const getEp = resolveEndpoint('workflows.applyGet', { workflowId: wf.id });
|
|
353
|
+
const data = await client.get(getEp.path, { service: getEp.service });
|
|
354
|
+
existing = data.data || data;
|
|
355
|
+
} catch (err) {
|
|
356
|
+
if (err.status !== 404) throw err;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (existing) {
|
|
360
|
+
const changes = buildApplyDiff(existing, wf);
|
|
361
|
+
if (changes.length === 0) {
|
|
362
|
+
printInfo(`No field-level changes detected for workflow ${wf.id}. Nothing to apply.`);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
printApplyDiff(wf.id, changes);
|
|
366
|
+
|
|
367
|
+
// fleet #468 item 3: on a non-TTY shell (no --force/-y) the old
|
|
368
|
+
// unconditional inquirer.prompt() call printed "User force closed
|
|
369
|
+
// the prompt with 0 null" and returned silently WITHOUT applying,
|
|
370
|
+
// which looked like success but wasn't. confirmDestructive() checks
|
|
371
|
+
// process.stdin.isTTY before ever importing inquirer, so a
|
|
372
|
+
// non-interactive shell without --force now fails loudly (exit
|
|
373
|
+
// code 1) naming --force, instead of no-op-ing.
|
|
374
|
+
const confirmed = await confirmDestructive(program, options, `Apply these changes to workflow ${wf.id}?`);
|
|
375
|
+
if (!confirmed) {
|
|
376
|
+
printInfo('Cancelled.');
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const putEp = resolveEndpoint('workflows.applyUpdate', { workflowId: wf.id });
|
|
381
|
+
const data = await client.put(putEp.path, { service: putEp.service, body: wf });
|
|
382
|
+
printEvent(glyphLine('quox', 'workflow updated', { result: `${wf.id} (quoxflow)`, status: 'ok' }));
|
|
383
|
+
formatDetail(data.data || data, { format: program.opts().output });
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// An id was given but nothing is stored under it yet — falls
|
|
388
|
+
// through to create below (POST also accepts a caller-supplied id).
|
|
389
|
+
printInfo(`No existing workflow found for id "${wf.id}" — creating.`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const createEp = resolveEndpoint('workflows.applyCreate');
|
|
393
|
+
const data = await client.post(createEp.path, { service: createEp.service, body: wf });
|
|
394
|
+
const created = data.data || data;
|
|
395
|
+
printEvent(events.created('quox', 'workflow', `${created.name || wf.name || created.id} (quoxflow) id:${created.id}`));
|
|
396
|
+
formatDetail(created, { format: program.opts().output });
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
workflow
|
|
400
|
+
.command('validate')
|
|
401
|
+
.description('Validate a workflow (quoxflow-direct). Give a stored workflow id to fetch and validate it (the same read path as `workflow get`), or -f <file> to validate the workflow stored under that file\'s "id" field. NOTE: this validates the STORED workflow, not local file content; run `workflow apply -f <file>` first to validate your latest edits.')
|
|
402
|
+
.argument('[workflowId]', 'stored workflow ID (fetched the same way `quox workflow get` does, then validated). Omit if using -f instead.')
|
|
403
|
+
.option('-f, --file <path>', 'workflow file — only its "id" field is used to look up the stored workflow')
|
|
404
|
+
.action(async (workflowId, options) => {
|
|
405
|
+
auth.requireAuth();
|
|
406
|
+
|
|
407
|
+
let targetId;
|
|
408
|
+
if (options.file) {
|
|
409
|
+
const wf = readDeclarativeFile(options.file);
|
|
410
|
+
if (!wf?.id) {
|
|
411
|
+
throw new Error(`${options.file} has no "id" field. \`workflow validate\` checks the STORED workflow by id, not local file content — apply the file first with \`quox workflow apply -f ${options.file}\`, or add an "id" matching an existing workflow.`);
|
|
412
|
+
}
|
|
413
|
+
targetId = wf.id;
|
|
414
|
+
} else if (workflowId) {
|
|
415
|
+
// fleet #468 item 1: `quox workflow validate <id>` previously required
|
|
416
|
+
// -f/--file even when the caller already had a stored workflow id.
|
|
417
|
+
// Fetch it through the same read path `quox workflow get` uses, so a
|
|
418
|
+
// missing id fails with the same "not found" signal that command gives.
|
|
419
|
+
const orgId = resolveOrgId(auth, program);
|
|
420
|
+
const getEp = resolveEndpoint('workflows.get', { orgId, workflowId });
|
|
421
|
+
try {
|
|
422
|
+
await client.get(getEp.path, { service: getEp.service });
|
|
423
|
+
} catch (err) {
|
|
424
|
+
if (err.status === 404) {
|
|
425
|
+
throw new Error(`Workflow "${workflowId}" was not found on the read path \`quox workflow get\` uses. Use -f <file> to validate a local file instead.`);
|
|
426
|
+
}
|
|
427
|
+
throw err;
|
|
428
|
+
}
|
|
429
|
+
targetId = workflowId;
|
|
430
|
+
} else {
|
|
431
|
+
throw new Error('Provide either a workflow id argument or -f <file>. Example: `quox workflow validate <id>`, or `quox workflow validate -f workflow.yaml`.');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const ep = resolveEndpoint('workflows.validate', { workflowId: targetId });
|
|
435
|
+
const data = await client.post(ep.path, { service: ep.service });
|
|
436
|
+
const result = data.data || data;
|
|
437
|
+
formatDetail(result, { format: program.opts().output, title: `Validation: ${targetId}` });
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
workflow
|
|
441
|
+
.command('import-n8n')
|
|
442
|
+
.description('Import an n8n workflow export as a quoxflow workflow')
|
|
443
|
+
.requiredOption('-f, --file <path>', 'path to an n8n workflow export (.yaml/.yml/.json)')
|
|
444
|
+
.option('--generate-ids', 'generate new node/workflow ids instead of reusing the n8n ones')
|
|
445
|
+
.option('--include-positions', 'preserve n8n canvas node positions')
|
|
446
|
+
.action(async (options) => {
|
|
447
|
+
auth.requireAuth();
|
|
448
|
+
const parsed = readDeclarativeFile(options.file);
|
|
449
|
+
// Accept either a raw n8n export, or an already-wrapped { workflow, options } body.
|
|
450
|
+
const n8nWorkflow = parsed && typeof parsed === 'object' && 'workflow' in parsed ? parsed.workflow : parsed;
|
|
451
|
+
const importOptions = { ...(parsed?.options || {}) };
|
|
452
|
+
if (options.generateIds) importOptions.generateIds = true;
|
|
453
|
+
if (options.includePositions) importOptions.includePositions = true;
|
|
454
|
+
|
|
455
|
+
const ep = resolveEndpoint('workflows.importN8n');
|
|
456
|
+
const data = await client.post(ep.path, {
|
|
457
|
+
service: ep.service,
|
|
458
|
+
body: { workflow: n8nWorkflow, options: Object.keys(importOptions).length ? importOptions : undefined },
|
|
459
|
+
});
|
|
460
|
+
const result = data.data || data;
|
|
461
|
+
printEvent(glyphLine('quox', 'n8n workflow imported', { result: result.workflow?.id || result.id, status: 'ok' }));
|
|
462
|
+
if (result.warnings?.length) {
|
|
463
|
+
for (const w of result.warnings) printWarning(w);
|
|
464
|
+
}
|
|
465
|
+
formatDetail(result.workflow || result, { format: program.opts().output });
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
workflow
|
|
469
|
+
.command('import-objective')
|
|
470
|
+
.description('Import an objective template as a quoxflow workflow')
|
|
471
|
+
.requiredOption('-f, --file <path>', 'path to an objective template (.yaml/.yml/.json) — a bare template object, or { template, schedule, options }')
|
|
472
|
+
.option('--cron <expr>', 'cron schedule expression (adds/overrides schedule.cron)')
|
|
473
|
+
.option('--timezone <tz>', 'schedule timezone (adds/overrides schedule.timezone)')
|
|
474
|
+
.option('--activate', 'activate the imported workflow (registers triggers)')
|
|
475
|
+
.option('--include-approval-gate', 'insert an approval gate node')
|
|
476
|
+
.action(async (options) => {
|
|
477
|
+
auth.requireAuth();
|
|
478
|
+
const parsed = readDeclarativeFile(options.file);
|
|
479
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
480
|
+
throw new Error(`${options.file} did not parse to an object.`);
|
|
481
|
+
}
|
|
482
|
+
const template = parsed.template && typeof parsed.template === 'object' ? parsed.template : parsed;
|
|
483
|
+
const schedule = { ...(parsed.schedule || {}) };
|
|
484
|
+
if (options.cron) schedule.cron = options.cron;
|
|
485
|
+
if (options.timezone) schedule.timezone = options.timezone;
|
|
486
|
+
const importOptions = { ...(parsed.options || {}) };
|
|
487
|
+
if (options.activate) importOptions.activate = true;
|
|
488
|
+
if (options.includeApprovalGate) importOptions.includeApprovalGate = true;
|
|
489
|
+
|
|
490
|
+
const body = { template };
|
|
491
|
+
if (schedule.cron) body.schedule = schedule;
|
|
492
|
+
if (Object.keys(importOptions).length) body.options = importOptions;
|
|
493
|
+
|
|
494
|
+
const ep = resolveEndpoint('workflows.importObjective');
|
|
495
|
+
const data = await client.post(ep.path, { service: ep.service, body });
|
|
496
|
+
const result = data.data || data;
|
|
497
|
+
printEvent(glyphLine('quox', 'objective template imported', { result: `${result.workflow?.id || result.id}${result.activated ? ' (activated)' : ''}`, status: 'ok' }));
|
|
498
|
+
if (result.warnings?.length) {
|
|
499
|
+
for (const w of result.warnings) printWarning(w);
|
|
500
|
+
}
|
|
501
|
+
formatDetail(result.workflow || result, { format: program.opts().output });
|
|
502
|
+
});
|
|
503
|
+
}
|