moshcode 0.25.1 → 0.27.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/.claude-plugin/marketplace.json +35 -0
- package/README.md +114 -0
- package/bin/moshcode.mjs +23 -1
- package/package.json +3 -1
- package/plugins/crypto/.claude-plugin/plugin.json +13 -0
- package/plugins/crypto/README.md +66 -0
- package/plugins/crypto/commands/bars.md +35 -0
- package/plugins/crypto/commands/book.md +31 -0
- package/plugins/crypto/commands/coin.md +31 -0
- package/plugins/crypto/commands/crypto.md +50 -0
- package/plugins/crypto/commands/pairs.md +31 -0
- package/plugins/crypto/commands/quote.md +31 -0
- package/plugins/crypto/commands/spark.md +36 -0
- package/plugins/ticker/.claude-plugin/plugin.json +13 -0
- package/plugins/ticker/README.md +49 -0
- package/plugins/ticker/commands/discover.md +37 -0
- package/plugins/ticker/commands/lookup.md +27 -0
- package/plugins/ticker/commands/reports.md +30 -0
- package/plugins/ticker/commands/research.md +29 -0
- package/plugins/ticker/commands/signals.md +30 -0
- package/plugins/ticker/commands/ticker.md +42 -0
- package/prd/0008-ticker-research-and-plugin-marketplace.md +130 -0
- package/prd/README.md +1 -0
- package/src/advisor.mjs +598 -0
- package/src/cli-schema.mjs +166 -0
- package/src/crypto.mjs +877 -0
- package/src/integrations.mjs +76 -2
- package/src/plugins.mjs +126 -0
- package/src/socials.mjs +72 -0
- package/src/tui.mjs +45 -1
package/src/integrations.mjs
CHANGED
|
@@ -8,8 +8,12 @@ import {
|
|
|
8
8
|
import {
|
|
9
9
|
SKILL_ENGINES, planSkillInstall, runSkillInstall, skillName,
|
|
10
10
|
} from "./skills.mjs";
|
|
11
|
+
import {
|
|
12
|
+
MARKETPLACE_NAME, PLUGINS, PLUGIN_ENGINES, marketplaceSource, planPluginCommand,
|
|
13
|
+
pluginId, resolvePlugin, runPluginCommand,
|
|
14
|
+
} from "./plugins.mjs";
|
|
11
15
|
import { catalogList, resolveCatalog } from "./mcp-catalog.mjs";
|
|
12
|
-
import { MCP_VERBS, SKILL_VERBS } from "./cli-schema.mjs";
|
|
16
|
+
import { MCP_VERBS, PLUGIN_VERBS, SKILL_VERBS } from "./cli-schema.mjs";
|
|
13
17
|
import { acid, ash, bone, ok, err, info } from "./ui.mjs";
|
|
14
18
|
|
|
15
19
|
function splitKV(pair) {
|
|
@@ -180,7 +184,7 @@ export function printSkillTargets(json = false) {
|
|
|
180
184
|
|
|
181
185
|
function summarize(results) {
|
|
182
186
|
for (const r of results) {
|
|
183
|
-
if (r.status === "added" || r.status === "installed") console.log(line(r.key, ok(r.status)));
|
|
187
|
+
if (r.status === "added" || r.status === "installed" || r.status === "removed") console.log(line(r.key, ok(r.status)));
|
|
184
188
|
else if (r.status === "failed") console.log(line(r.key, err(`failed${r.code != null ? ` (code ${r.code})` : r.signal ? ` (${r.signal})` : ""}`)));
|
|
185
189
|
else if (r.status === "not-installed") console.log(line(r.key, ash("not installed — /install " + r.key)));
|
|
186
190
|
else console.log(line(r.key, ash(`skipped — ${r.reason}`)));
|
|
@@ -263,3 +267,73 @@ export async function skillCommand(tokens, { run, installedSet } = {}) {
|
|
|
263
267
|
summarize(results);
|
|
264
268
|
return anyFailed(results) ? 1 : 0;
|
|
265
269
|
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* `/plugin list` — what this marketplace ships, and which engines can take it.
|
|
273
|
+
*
|
|
274
|
+
* Two tables rather than one: the plugin list is a property of moshcode, the
|
|
275
|
+
* engine support is a property of this machine, and merging them into a single
|
|
276
|
+
* list is how "installed" and "installable" get confused.
|
|
277
|
+
*/
|
|
278
|
+
export function printPluginTargets(json = false, { installedSet } = {}) {
|
|
279
|
+
const targets = integrationTargetStatus(PLUGIN_ENGINES, { installedSet }).map((t) => ({
|
|
280
|
+
...t, supported: PLUGIN_ENGINES.includes(t.name),
|
|
281
|
+
}));
|
|
282
|
+
if (json) {
|
|
283
|
+
console.log(JSON.stringify({
|
|
284
|
+
marketplace: { name: MARKETPLACE_NAME, source: marketplaceSource() },
|
|
285
|
+
plugins: PLUGINS,
|
|
286
|
+
engines: targets,
|
|
287
|
+
}, null, 2));
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
console.log(bone(" plugins") + ash(" — install moshcode's slash commands with ") + acid("/plugin install"));
|
|
291
|
+
for (const plugin of PLUGINS) {
|
|
292
|
+
console.log(` ${acid(pluginId(plugin.name).padEnd(18))}${ash(plugin.description)}`);
|
|
293
|
+
console.log(` ${" ".repeat(18)}${ash(plugin.commands.join(" "))}`);
|
|
294
|
+
}
|
|
295
|
+
console.log("");
|
|
296
|
+
for (const target of targets) {
|
|
297
|
+
const dot = target.supported && target.installed ? DOT.installed : DOT.missing;
|
|
298
|
+
console.log(` ${dot} ${bone(target.name.padEnd(9))} ${ash(target.supported ? "plugins supported" : "no plugin primitive")}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Run `/plugin …`. `tokens` are the words after `plugin`. */
|
|
303
|
+
export async function pluginCommand(tokens, { run, installedSet } = {}) {
|
|
304
|
+
const verb = tokens[0];
|
|
305
|
+
if (!verb || verb === "list") {
|
|
306
|
+
printPluginTargets(tokens.slice(1).includes("--json"), { installedSet });
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
if (verb !== "install" && verb !== "remove") {
|
|
310
|
+
console.log(err(`unknown plugin verb "${verb}" — try ${PLUGIN_VERBS.map(({ name }) => name).join(", ")}`));
|
|
311
|
+
return 1;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const rest = tokens.slice(1).filter((t) => t !== "--json");
|
|
315
|
+
const stray = rest.find((t) => String(t).startsWith("-"));
|
|
316
|
+
if (stray) { console.log(err(`unknown plugin flag "${stray}"`)); return 1; }
|
|
317
|
+
|
|
318
|
+
const plugin = resolvePlugin(rest[0]);
|
|
319
|
+
if (!plugin) {
|
|
320
|
+
console.log(err(`unknown plugin "${rest[0]}" — this marketplace ships ${PLUGINS.map((p) => p.name).join(", ")}`));
|
|
321
|
+
return 1;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const source = marketplaceSource();
|
|
325
|
+
console.log(verb === "install"
|
|
326
|
+
? info(`installing ${bone(pluginId(plugin.name))} ${ash(`from ${source}`)} across plugin engines…`)
|
|
327
|
+
: info(`removing ${bone(pluginId(plugin.name))} from plugin engines…`));
|
|
328
|
+
|
|
329
|
+
const plan = planPluginCommand({ plugin, source }, { installedSet, verb });
|
|
330
|
+
const results = await runPluginCommand(plan, { verb, ...(run ? { run } : {}) });
|
|
331
|
+
summarize(results);
|
|
332
|
+
|
|
333
|
+
// A newly installed plugin is not live in an already-running engine, and the
|
|
334
|
+
// first thing anyone does is type the slash command and conclude it failed.
|
|
335
|
+
if (!anyFailed(results) && verb === "install" && results.some((r) => r.status === "installed")) {
|
|
336
|
+
console.log(info(`restart the engine, then try ${acid(`${plugin.commands[0]} NVDA`)}`));
|
|
337
|
+
}
|
|
338
|
+
return anyFailed(results) ? 1 : 0;
|
|
339
|
+
}
|
package/src/plugins.mjs
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// `moshcode plugin` — install moshcode's own slash commands into an engine.
|
|
2
|
+
//
|
|
3
|
+
// The same shape as src/skills.mjs, for the same reason: one source, a plan of
|
|
4
|
+
// per-engine actions, and a summary that names the engines it *skipped* as well
|
|
5
|
+
// as the ones it touched. An engine silently missing from the summary reads as
|
|
6
|
+
// "installed everywhere", which is exactly the confusion prd/0003 R8 exists to
|
|
7
|
+
// prevent.
|
|
8
|
+
//
|
|
9
|
+
// Claude Code is currently the only engine with a plugin primitive. That is a
|
|
10
|
+
// fact about the engines, not an assumption baked into the fan-out — adding a
|
|
11
|
+
// second one means adding a case to pluginInstallActions, nothing else.
|
|
12
|
+
import { ENGINES, isInstalled, ranOk, runCmd } from "./engines.mjs";
|
|
13
|
+
|
|
14
|
+
/** Engines with a plugin primitive. */
|
|
15
|
+
export const PLUGIN_ENGINES = ["claude"];
|
|
16
|
+
|
|
17
|
+
/** The marketplace this repo publishes (see .claude-plugin/marketplace.json). */
|
|
18
|
+
export const MARKETPLACE_NAME = "moshcode";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Where the marketplace is fetched from. A GitHub `owner/repo` by default;
|
|
22
|
+
* point it at a checkout to test an unreleased plugin:
|
|
23
|
+
* MOSHCODE_PLUGIN_SOURCE=. moshcode plugin install
|
|
24
|
+
*/
|
|
25
|
+
export function marketplaceSource(env = process.env) {
|
|
26
|
+
return String(env.MOSHCODE_PLUGIN_SOURCE || "moshcoder/moshcode").trim() || "moshcoder/moshcode";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The plugins this marketplace ships. Mirrors .claude-plugin/marketplace.json. */
|
|
30
|
+
export const PLUGINS = [
|
|
31
|
+
{
|
|
32
|
+
name: "ticker",
|
|
33
|
+
description: "equity research slash commands backed by advis0r.com",
|
|
34
|
+
commands: ["/ticker", "/signals", "/research", "/lookup", "/reports", "/discover"],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "crypto",
|
|
38
|
+
description: "crypto market data slash commands backed by advis0r.com",
|
|
39
|
+
commands: ["/crypto", "/quote", "/book", "/bars", "/spark", "/pairs", "/coin"],
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
export const DEFAULT_PLUGIN = PLUGINS[0].name;
|
|
44
|
+
|
|
45
|
+
export function resolvePlugin(name) {
|
|
46
|
+
if (!name) return PLUGINS.find((p) => p.name === DEFAULT_PLUGIN) ?? null;
|
|
47
|
+
const key = String(name).toLowerCase().replace(/@.*$/, "");
|
|
48
|
+
return PLUGINS.find((p) => p.name === key) ?? null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Fully-qualified plugin id, the form `claude plugin install` disambiguates with. */
|
|
52
|
+
export function pluginId(name) {
|
|
53
|
+
return `${name}@${MARKETPLACE_NAME}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The commands one engine needs to install a plugin.
|
|
58
|
+
*
|
|
59
|
+
* Adding the marketplace is idempotent and separate from installing, so it runs
|
|
60
|
+
* every time: a machine that added the marketplace before this plugin existed
|
|
61
|
+
* would otherwise fail the install with "not found in any marketplace".
|
|
62
|
+
*/
|
|
63
|
+
export function pluginInstallActions(key, { plugin, source, scope }) {
|
|
64
|
+
switch (key) {
|
|
65
|
+
case "claude":
|
|
66
|
+
return [
|
|
67
|
+
{ cmd: "claude", args: ["plugin", "marketplace", "add", source, ...(scope ? ["--scope", scope] : [])] },
|
|
68
|
+
{ cmd: "claude", args: ["plugin", "install", pluginId(plugin.name), ...(scope ? ["--scope", scope] : [])] },
|
|
69
|
+
];
|
|
70
|
+
default:
|
|
71
|
+
return { skip: "no plugin primitive" };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function pluginRemoveActions(key, { plugin }) {
|
|
76
|
+
switch (key) {
|
|
77
|
+
case "claude":
|
|
78
|
+
return [{ cmd: "claude", args: ["plugin", "uninstall", pluginId(plugin.name)] }];
|
|
79
|
+
default:
|
|
80
|
+
return { skip: "no plugin primitive" };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Plan the fan-out: one entry per engine, with its actions or its skip reason.
|
|
86
|
+
* Derived from ENGINES so an engine added later cannot fall out of the summary.
|
|
87
|
+
*/
|
|
88
|
+
export function planPluginCommand(spec, { installedSet, verb = "install" } = {}) {
|
|
89
|
+
const build = verb === "remove" ? pluginRemoveActions : pluginInstallActions;
|
|
90
|
+
const rest = Object.keys(ENGINES).filter((key) => !PLUGIN_ENGINES.includes(key));
|
|
91
|
+
return [...PLUGIN_ENGINES, ...rest].map((key) => {
|
|
92
|
+
const bin = ENGINES[key].bin;
|
|
93
|
+
const installed = installedSet ? installedSet.has(key) : isInstalled(bin, ENGINES[key].binDirs);
|
|
94
|
+
const actions = build(key, spec);
|
|
95
|
+
return Array.isArray(actions)
|
|
96
|
+
? { key, bin, installed, actions }
|
|
97
|
+
: { key, bin, installed, ...actions };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Execute a plan. Returns [{ key, status, reason?, code? }] with
|
|
103
|
+
* status one of installed | removed | skipped | failed | not-installed.
|
|
104
|
+
* `run` is injectable for tests.
|
|
105
|
+
*/
|
|
106
|
+
export async function runPluginCommand(plan, { run = runCmd, verb = "install" } = {}) {
|
|
107
|
+
const done = verb === "remove" ? "removed" : "installed";
|
|
108
|
+
const results = [];
|
|
109
|
+
for (const item of plan) {
|
|
110
|
+
if (item.skip) { results.push({ key: item.key, status: "skipped", reason: item.skip }); continue; }
|
|
111
|
+
if (!item.installed) { results.push({ key: item.key, status: "not-installed" }); continue; }
|
|
112
|
+
let last = null;
|
|
113
|
+
let failed = false;
|
|
114
|
+
for (const action of item.actions) {
|
|
115
|
+
last = await run(action.cmd, action.args);
|
|
116
|
+
if (!ranOk(last)) { failed = true; break; }
|
|
117
|
+
}
|
|
118
|
+
results.push({
|
|
119
|
+
key: item.key,
|
|
120
|
+
status: failed ? "failed" : done,
|
|
121
|
+
code: last?.code ?? null,
|
|
122
|
+
signal: last?.signal ?? null,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return results;
|
|
126
|
+
}
|
package/src/socials.mjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { canOpenBrowser, openBrowser } from "./open-url.mjs";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_APP = "https://app.moshcode.sh";
|
|
4
|
+
|
|
5
|
+
export const SOCIALS = [
|
|
6
|
+
{
|
|
7
|
+
name: "bluesky",
|
|
8
|
+
aliases: ["bsky"],
|
|
9
|
+
description: "official Bluesky browser composer",
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: "nostr",
|
|
13
|
+
aliases: [],
|
|
14
|
+
description: "NIP-07/NIP-46 browser signer + relay publish",
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function resolveSocial(name) {
|
|
19
|
+
const wanted = String(name ?? "").trim().toLowerCase();
|
|
20
|
+
return SOCIALS.find((social) =>
|
|
21
|
+
social.name === wanted || social.aliases.includes(wanted)) ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function appOrigin(env = process.env) {
|
|
25
|
+
return String(env.MOSHCODE_API || DEFAULT_APP).replace(/\/+$/, "");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build the browser hand-off without opening anything. Nostr keeps the draft
|
|
30
|
+
* in the fragment so it never reaches app.moshcode.sh access logs or Referer
|
|
31
|
+
* headers; the composer reads it entirely in the browser.
|
|
32
|
+
*/
|
|
33
|
+
export function socialPostUrl(name, message, { env = process.env } = {}) {
|
|
34
|
+
const social = resolveSocial(name);
|
|
35
|
+
if (!social) return null;
|
|
36
|
+
const text = String(message ?? "");
|
|
37
|
+
if (social.name === "bluesky") {
|
|
38
|
+
return `https://bsky.app/intent/compose?${new URLSearchParams({ text })}`;
|
|
39
|
+
}
|
|
40
|
+
return `${appOrigin(env)}/socials/nostr#${new URLSearchParams({ text })}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function socialRoster() {
|
|
44
|
+
return SOCIALS.map((social) => ({ ...social, aliases: [...social.aliases] }));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Open a provider composer. Posting remains an explicit browser confirmation:
|
|
49
|
+
* Bluesky requires it, and Nostr asks the browser signer before relay publish.
|
|
50
|
+
*/
|
|
51
|
+
export function postSocial(args, {
|
|
52
|
+
env = process.env,
|
|
53
|
+
canOpen = canOpenBrowser,
|
|
54
|
+
open = openBrowser,
|
|
55
|
+
} = {}) {
|
|
56
|
+
const [requested, ...words] = Array.isArray(args) ? args : [];
|
|
57
|
+
const social = resolveSocial(requested);
|
|
58
|
+
if (!requested) return { ok: false, error: 'usage: /post <social> "message"' };
|
|
59
|
+
if (!social) {
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: `unknown social "${requested}". try: ${SOCIALS.map((entry) => entry.name).join(", ")}`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const message = words.join(" ").trim();
|
|
67
|
+
if (!message) return { ok: false, error: 'usage: /post <social> "message"' };
|
|
68
|
+
|
|
69
|
+
const url = socialPostUrl(social.name, message, { env });
|
|
70
|
+
const opened = Boolean(canOpen() && open(url));
|
|
71
|
+
return { ok: true, social: social.name, message, url, opened };
|
|
72
|
+
}
|
package/src/tui.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import path from "node:path";
|
|
|
10
10
|
import { ENGINES, agentLaunchArgs, resolveEngine, engineStatus, openSession } from "./engines.mjs";
|
|
11
11
|
import { TOOLS, resolveTool, toolStatus, openTool } from "./tools.mjs";
|
|
12
12
|
import { tradeArgs, tradeUsage } from "./trade.mjs";
|
|
13
|
+
import { postSocial, socialRoster } from "./socials.mjs";
|
|
13
14
|
import { runUpgrade } from "./upgrade.mjs";
|
|
14
15
|
import { locate, tilde } from "./pwd.mjs";
|
|
15
16
|
import { createPrd, listPrds, authoringPrompt } from "./prd.mjs";
|
|
@@ -18,7 +19,10 @@ import { createMirror, teeOutput } from "./mirror.mjs";
|
|
|
18
19
|
import { fetchMotdAd } from "./ads.mjs";
|
|
19
20
|
import { runScript } from "./runtime.mjs";
|
|
20
21
|
import { moshVocabulary } from "./commands.mjs";
|
|
21
|
-
import { mcpCommand, skillCommand } from "./integrations.mjs";
|
|
22
|
+
import { mcpCommand, pluginCommand, skillCommand } from "./integrations.mjs";
|
|
23
|
+
import { tickerCommand } from "./advisor.mjs";
|
|
24
|
+
import { cryptoCommand } from "./crypto.mjs";
|
|
25
|
+
import { canOpenBrowser, openBrowser } from "./open-url.mjs";
|
|
22
26
|
import { banner, hr, acid, ash, bone, dim, ok, err, warn, info, moshcodeVersion } from "./ui.mjs";
|
|
23
27
|
import { CORE_CLI_COMMAND_NAMES } from "./cli-schema.mjs";
|
|
24
28
|
import { findPitCommand, pitHelpModel, renderPitCommand, suggest, wantsHelp } from "./help.mjs";
|
|
@@ -156,6 +160,15 @@ function printTools() {
|
|
|
156
160
|
console.log(ash(" → ") + acid("https://dev.profullstack.com/"));
|
|
157
161
|
}
|
|
158
162
|
|
|
163
|
+
function printSocials() {
|
|
164
|
+
console.log(bone(" socials") + ash(" — compose with ") + acid('/post <social> "message"'));
|
|
165
|
+
for (const social of socialRoster()) {
|
|
166
|
+
const aliases = social.aliases.length ? ` (${social.aliases.join(", ")})` : "";
|
|
167
|
+
console.log(` ${acid("●")} ${bone(social.name.padEnd(9))} ${ash(social.description + aliases)}`);
|
|
168
|
+
}
|
|
169
|
+
console.log(ash(" the browser always asks you to confirm before anything is published"));
|
|
170
|
+
}
|
|
171
|
+
|
|
159
172
|
/**
|
|
160
173
|
* The moshscript vocabulary, split the way the CLI's help splits it.
|
|
161
174
|
*
|
|
@@ -660,6 +673,37 @@ export async function tui() {
|
|
|
660
673
|
rl = mkrl();
|
|
661
674
|
continue;
|
|
662
675
|
}
|
|
676
|
+
// `/ticker` renders in the pit rather than handing the terminal to a tool:
|
|
677
|
+
// there is no advis0r binary to launch, only a public read-only API.
|
|
678
|
+
if (cmd === "ticker" || cmd === "advisor") {
|
|
679
|
+
await tickerCommand(rest, { openUrl: (url) => canOpenBrowser() && openBrowser(url) });
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
// `/crypto` renders in the pit for the same reason `/ticker` does: there is
|
|
683
|
+
// no crypto binary to hand the terminal to, only a public read-only API.
|
|
684
|
+
if (cmd === "crypto" || cmd === "coins") {
|
|
685
|
+
await cryptoCommand(rest, { openUrl: (url) => canOpenBrowser() && openBrowser(url) });
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (cmd === "plugin" || cmd === "plugins") {
|
|
689
|
+
await pluginCommand(rest);
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (cmd === "socials" || cmd === "social") {
|
|
693
|
+
printSocials();
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (cmd === "post") {
|
|
697
|
+
const result = postSocial(rest);
|
|
698
|
+
if (!result.ok) { console.log(err(result.error)); continue; }
|
|
699
|
+
if (result.opened) {
|
|
700
|
+
console.log(ok(`opened the ${result.social} composer — confirm the post in your browser 🤘`));
|
|
701
|
+
} else {
|
|
702
|
+
console.log(info(`open this ${result.social} composer in a browser:`));
|
|
703
|
+
console.log(` ${result.url}`);
|
|
704
|
+
}
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
663
707
|
// Bare engine name → open it.
|
|
664
708
|
const resolved = resolveEngine(cmd);
|
|
665
709
|
if (resolved) {
|