@muxoai/cli 0.1.0 → 0.1.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/README.md +1 -0
- package/dist/index.js +580 -233
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ muxo plan Server-side diff → ordered op list
|
|
|
34
34
|
muxo apply Push manifest, snapshot version
|
|
35
35
|
muxo add <bundle> Append a bundle stanza
|
|
36
36
|
muxo run <workflow> Execute a workflow one-shot
|
|
37
|
+
muxo call <capability> Invoke a capability directly
|
|
37
38
|
muxo status Project, bundles, budget
|
|
38
39
|
muxo keys list|create|rotate|revoke
|
|
39
40
|
muxo credits balance|packs|buy <pack>
|
package/dist/index.js
CHANGED
|
@@ -4,8 +4,148 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/init.ts
|
|
7
|
-
import { access, writeFile } from "node:fs/promises";
|
|
7
|
+
import { access, mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
8
8
|
import { basename } from "node:path";
|
|
9
|
+
import { validateManifest } from "@muxoai/core";
|
|
10
|
+
|
|
11
|
+
// src/lib/api.ts
|
|
12
|
+
async function request(opts, method, path, body) {
|
|
13
|
+
if (opts.debug) {
|
|
14
|
+
console.error(`muxo: ${method} ${opts.baseUrl}${path}`);
|
|
15
|
+
}
|
|
16
|
+
let res;
|
|
17
|
+
try {
|
|
18
|
+
res = await fetch(`${opts.baseUrl}${path}`, {
|
|
19
|
+
method,
|
|
20
|
+
headers: {
|
|
21
|
+
...opts.key ? { authorization: `Bearer ${opts.key}` } : {},
|
|
22
|
+
"content-type": "application/json"
|
|
23
|
+
},
|
|
24
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
25
|
+
});
|
|
26
|
+
} catch (err) {
|
|
27
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
error: {
|
|
31
|
+
code: "internal",
|
|
32
|
+
message: `network error reaching ${opts.baseUrl}${path}: ${reason}. Check your connection and retry (default endpoint: https://api.muxo.ai/v1)`
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const envelope = await res.json();
|
|
38
|
+
if (!envelope.ok && res.status === 401 && opts.key === void 0) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
error: {
|
|
42
|
+
code: envelope.error.code,
|
|
43
|
+
message: `${envelope.error.message} \u2014 no key found: run \`muxo keys create\` for a free one, or set MUXO_KEY`
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return envelope;
|
|
48
|
+
} catch {
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: { code: "internal", message: `muxo returned HTTP ${res.status} with a non-JSON body` }
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function makeApi(opts) {
|
|
56
|
+
return {
|
|
57
|
+
get: (path) => request(opts, "GET", path),
|
|
58
|
+
post: (path, body) => request(opts, "POST", path, body),
|
|
59
|
+
put: (path, body) => request(opts, "PUT", path, body),
|
|
60
|
+
del: (path) => request(opts, "DELETE", path)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// src/lib/config.ts
|
|
65
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
66
|
+
|
|
67
|
+
// src/lib/keychain.ts
|
|
68
|
+
import { execFile } from "node:child_process";
|
|
69
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
70
|
+
import { homedir } from "node:os";
|
|
71
|
+
import { dirname, join } from "node:path";
|
|
72
|
+
import { promisify } from "node:util";
|
|
73
|
+
var run = promisify(execFile);
|
|
74
|
+
var SERVICE = "muxo";
|
|
75
|
+
var ACCOUNT = "muxo-key";
|
|
76
|
+
var KEY_FILE = join(homedir(), ".muxo", "key");
|
|
77
|
+
async function keychainGet() {
|
|
78
|
+
if (process.platform === "darwin") {
|
|
79
|
+
try {
|
|
80
|
+
const { stdout } = await run("security", [
|
|
81
|
+
"find-generic-password",
|
|
82
|
+
"-s",
|
|
83
|
+
SERVICE,
|
|
84
|
+
"-a",
|
|
85
|
+
ACCOUNT,
|
|
86
|
+
"-w"
|
|
87
|
+
]);
|
|
88
|
+
const key = stdout.trim();
|
|
89
|
+
if (key) return key;
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const key = (await readFile(KEY_FILE, "utf8")).trim();
|
|
95
|
+
return key || void 0;
|
|
96
|
+
} catch {
|
|
97
|
+
return void 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function keychainSet(key) {
|
|
101
|
+
if (process.platform === "darwin") {
|
|
102
|
+
try {
|
|
103
|
+
await run("security", [
|
|
104
|
+
"add-generic-password",
|
|
105
|
+
"-U",
|
|
106
|
+
"-s",
|
|
107
|
+
SERVICE,
|
|
108
|
+
"-a",
|
|
109
|
+
ACCOUNT,
|
|
110
|
+
"-w",
|
|
111
|
+
key
|
|
112
|
+
]);
|
|
113
|
+
return "keychain";
|
|
114
|
+
} catch {
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
await mkdir(dirname(KEY_FILE), { recursive: true });
|
|
118
|
+
await writeFile(KEY_FILE, `${key}
|
|
119
|
+
`, { mode: 384 });
|
|
120
|
+
await chmod(KEY_FILE, 384);
|
|
121
|
+
return "file";
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/lib/config.ts
|
|
125
|
+
var DEFAULT_BASE_URL = "https://api.muxo.ai/v1";
|
|
126
|
+
function normalizeBaseUrl(raw) {
|
|
127
|
+
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
128
|
+
if (trimmed === "") return DEFAULT_BASE_URL;
|
|
129
|
+
return /\/v1$/.test(trimmed) ? trimmed : `${trimmed}/v1`;
|
|
130
|
+
}
|
|
131
|
+
function baseUrl() {
|
|
132
|
+
return normalizeBaseUrl(process.env.MUXO_API_BASE || DEFAULT_BASE_URL);
|
|
133
|
+
}
|
|
134
|
+
async function resolveKey() {
|
|
135
|
+
if (process.env.MUXO_KEY) return process.env.MUXO_KEY;
|
|
136
|
+
return keychainGet();
|
|
137
|
+
}
|
|
138
|
+
var MANIFEST_FILES = ["muxo.yaml", "muxo.muxo", ".muxo/muxo.yaml"];
|
|
139
|
+
async function readManifestText() {
|
|
140
|
+
for (const file of MANIFEST_FILES) {
|
|
141
|
+
try {
|
|
142
|
+
return await readFile2(file, "utf8");
|
|
143
|
+
} catch {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return void 0;
|
|
148
|
+
}
|
|
9
149
|
|
|
10
150
|
// src/lib/templates.ts
|
|
11
151
|
import { stringify } from "yaml";
|
|
@@ -18,6 +158,8 @@ var KNOWN_BUNDLES = [
|
|
|
18
158
|
{ name: "compute", capabilities: ["compute.sandbox", "compute.container"] },
|
|
19
159
|
{ name: "deploy", capabilities: ["deploy.site", "deploy.container", "deploy.edge"] },
|
|
20
160
|
{ name: "domains", capabilities: ["domains.register", "dns.manage"] },
|
|
161
|
+
{ name: "data", capabilities: ["db.write", "db.query"] },
|
|
162
|
+
{ name: "kv", capabilities: ["kv.store"] },
|
|
21
163
|
{ name: "storage", capabilities: ["storage.object", "vector.search"] },
|
|
22
164
|
{ name: "ops", capabilities: ["observability.log", "queue.emit", "email.send"] }
|
|
23
165
|
];
|
|
@@ -30,28 +172,50 @@ var webIntel = {
|
|
|
30
172
|
var llm = {
|
|
31
173
|
bundles: { llm: { capabilities: ["llm.chat", "llm.embed"] } }
|
|
32
174
|
};
|
|
175
|
+
var deploySite = {
|
|
176
|
+
bundles: { deploy: { capabilities: ["deploy.site", "deploy.edge"] } }
|
|
177
|
+
};
|
|
178
|
+
var storage = {
|
|
179
|
+
bundles: { storage: { capabilities: ["storage.object", "vector.search"] } }
|
|
180
|
+
};
|
|
181
|
+
var data = {
|
|
182
|
+
bundles: { data: { capabilities: ["db.write", "db.query"] } }
|
|
183
|
+
};
|
|
184
|
+
var kv = {
|
|
185
|
+
bundles: { kv: { capabilities: ["kv.store"] } }
|
|
186
|
+
};
|
|
187
|
+
var ops = {
|
|
188
|
+
bundles: { ops: { capabilities: ["observability.log", "queue.emit", "email.send"] } }
|
|
189
|
+
};
|
|
190
|
+
var compute = {
|
|
191
|
+
bundles: { compute: { capabilities: ["compute.sandbox", "compute.container"] } }
|
|
192
|
+
};
|
|
193
|
+
var browser = {
|
|
194
|
+
bundles: { browser: { capabilities: ["browser.drive"] } }
|
|
195
|
+
};
|
|
196
|
+
var domains = {
|
|
197
|
+
bundles: { domains: { capabilities: ["domains.register", "dns.manage"] } }
|
|
198
|
+
};
|
|
33
199
|
var priceRadar = {
|
|
34
|
-
bundles: {
|
|
200
|
+
bundles: {
|
|
201
|
+
"web-intel": { capabilities: ["web.search", "web.scrape"] }
|
|
202
|
+
},
|
|
35
203
|
workflows: {
|
|
36
204
|
"daily-prices": {
|
|
37
205
|
on: { schedule: "0 6 * * *" },
|
|
38
206
|
steps: [
|
|
39
|
-
|
|
40
|
-
{
|
|
207
|
+
// web.search resolves to an array of {url, title, markdown}
|
|
208
|
+
{ search: { cap: "web.search", query: "competitor pricing", limit: 5, into: "results" } },
|
|
209
|
+
// fan out: scrape each result page
|
|
41
210
|
{
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
into: "prices"
|
|
211
|
+
each: {
|
|
212
|
+
over: "${results}",
|
|
213
|
+
as: "item",
|
|
214
|
+
steps: [{ scrape: { cap: "web.scrape", url: "${item.url}", into: "page" } }]
|
|
47
215
|
}
|
|
48
|
-
}
|
|
49
|
-
{ store: { cap: "db.write", into: "stack.database.main.prices" } }
|
|
216
|
+
}
|
|
50
217
|
]
|
|
51
218
|
}
|
|
52
|
-
},
|
|
53
|
-
stack: {
|
|
54
|
-
database: { main: { provider: "neon", service: "postgres" } }
|
|
55
219
|
}
|
|
56
220
|
};
|
|
57
221
|
function slugify(name) {
|
|
@@ -77,34 +241,63 @@ function matchTemplate(intent) {
|
|
|
77
241
|
const lower = intent.toLowerCase();
|
|
78
242
|
const bundles = {};
|
|
79
243
|
let workflows;
|
|
80
|
-
|
|
81
|
-
let schedule = detectSchedule(intent);
|
|
244
|
+
const schedule = detectSchedule(intent);
|
|
82
245
|
if (/\bprice|pricing\b/.test(lower)) {
|
|
83
246
|
return priceRadar;
|
|
84
247
|
}
|
|
85
|
-
if (
|
|
248
|
+
if (/\bhost(ing)?\b|\bdeploy|\blanding\b|\bwebsite\b|web ?site|static site|\bpublish\b|\bserve\b/.test(lower)) {
|
|
249
|
+
Object.assign(bundles, deploySite.bundles);
|
|
250
|
+
}
|
|
251
|
+
if (/\btrack|monitor|scrape|search|extract|crawl|\bweb\b|news|hacker|\bhn\b/.test(lower)) {
|
|
86
252
|
Object.assign(bundles, webIntel.bundles);
|
|
87
253
|
}
|
|
88
|
-
if (
|
|
254
|
+
if (/\bchat|llm|summar|embed|\bai\b|assistant|generate\b/.test(lower)) {
|
|
89
255
|
Object.assign(bundles, llm.bundles);
|
|
90
256
|
}
|
|
91
|
-
if (
|
|
92
|
-
|
|
257
|
+
if (/\bstore|save|database|postgres|sql|record|table\b/.test(lower)) {
|
|
258
|
+
Object.assign(bundles, data.bundles);
|
|
259
|
+
}
|
|
260
|
+
if (/\bkv|cache|config|settings|feature flag\b/.test(lower)) {
|
|
261
|
+
Object.assign(bundles, kv.bundles);
|
|
262
|
+
}
|
|
263
|
+
if (/\bupload|\bfile|asset|object|bucket\b/.test(lower)) {
|
|
264
|
+
Object.assign(bundles, storage.bundles);
|
|
265
|
+
}
|
|
266
|
+
if (/\bvector|embedding|semantic|similarity|\brag\b|recommend\b/.test(lower)) {
|
|
267
|
+
Object.assign(bundles, storage.bundles);
|
|
93
268
|
}
|
|
94
|
-
if (
|
|
269
|
+
if (/\bemail|notif|alert|notify|\bmail\b/.test(lower)) {
|
|
270
|
+
Object.assign(bundles, ops.bundles);
|
|
271
|
+
}
|
|
272
|
+
if (/\bsandbox|execute|run code|compute|container|docker|\bcode\b/.test(lower)) {
|
|
273
|
+
Object.assign(bundles, compute.bundles);
|
|
274
|
+
}
|
|
275
|
+
if (/\bbrowser|automate|click|login form|\bform\b/.test(lower)) {
|
|
276
|
+
Object.assign(bundles, browser.bundles);
|
|
277
|
+
}
|
|
278
|
+
if (/\bdomain|\bdns\b/.test(lower)) {
|
|
279
|
+
Object.assign(bundles, domains.bundles);
|
|
280
|
+
}
|
|
281
|
+
if (schedule !== void 0 && bundles["web-intel"] !== void 0) {
|
|
95
282
|
const topic = (lower.match(/[a-z][a-z-]{3,}/)?.[0] ?? "watch").slice(0, 20);
|
|
96
283
|
workflows = {
|
|
97
284
|
[`${topic.replace(/-+$/, "")}-watch`]: {
|
|
98
285
|
on: { schedule },
|
|
99
286
|
steps: [
|
|
100
287
|
{ search: { cap: "web.search", query: intent.slice(0, 80), limit: 3, into: "results" } },
|
|
101
|
-
{
|
|
288
|
+
{
|
|
289
|
+
each: {
|
|
290
|
+
over: "${results}",
|
|
291
|
+
as: "item",
|
|
292
|
+
steps: [{ scrape: { cap: "web.scrape", url: "${item.url}", into: "page" } }]
|
|
293
|
+
}
|
|
294
|
+
}
|
|
102
295
|
]
|
|
103
296
|
}
|
|
104
297
|
};
|
|
105
298
|
}
|
|
106
299
|
if (Object.keys(bundles).length === 0) return { bundles: {} };
|
|
107
|
-
return { bundles, workflows
|
|
300
|
+
return { bundles, workflows };
|
|
108
301
|
}
|
|
109
302
|
function renderManifest(project, description, tpl) {
|
|
110
303
|
const doc = {
|
|
@@ -177,102 +370,93 @@ async function exists(path) {
|
|
|
177
370
|
return false;
|
|
178
371
|
}
|
|
179
372
|
}
|
|
373
|
+
function landingPage(title) {
|
|
374
|
+
return `<!doctype html>
|
|
375
|
+
<html lang="en">
|
|
376
|
+
<head>
|
|
377
|
+
<meta charset="utf-8" />
|
|
378
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
379
|
+
<title>${title}</title>
|
|
380
|
+
<style>
|
|
381
|
+
:root { color-scheme: dark; }
|
|
382
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center;
|
|
383
|
+
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
384
|
+
background: #0b0b12; color: #f5f5f7; }
|
|
385
|
+
main { text-align: center; padding: 2rem; }
|
|
386
|
+
h1 { font-size: clamp(2rem, 6vw, 4rem); margin: 0 0 0.5rem; }
|
|
387
|
+
p { color: #a1a1aa; margin: 0; }
|
|
388
|
+
</style>
|
|
389
|
+
</head>
|
|
390
|
+
<body>
|
|
391
|
+
<main>
|
|
392
|
+
<h1>${title}</h1>
|
|
393
|
+
<p>Deployed with muxo.</p>
|
|
394
|
+
</main>
|
|
395
|
+
</body>
|
|
396
|
+
</html>
|
|
397
|
+
`;
|
|
398
|
+
}
|
|
399
|
+
async function architectManifest(intent, project, opts) {
|
|
400
|
+
const key = await resolveKey();
|
|
401
|
+
if (key === void 0) return void 0;
|
|
402
|
+
const api = makeApi({ baseUrl: baseUrl(), key, debug: opts.debug });
|
|
403
|
+
const res = await api.post("/architect", { intent, project });
|
|
404
|
+
if (!res.ok) return void 0;
|
|
405
|
+
const data2 = res.data;
|
|
406
|
+
if (typeof data2?.yaml !== "string" || data2.yaml.trim() === "") return void 0;
|
|
407
|
+
return { yaml: data2.yaml, source: "architect" };
|
|
408
|
+
}
|
|
180
409
|
async function init(intent, opts) {
|
|
181
410
|
if (await exists("muxo.yaml")) {
|
|
182
411
|
console.error("error: muxo.yaml already exists in this directory");
|
|
183
412
|
return 1;
|
|
184
413
|
}
|
|
185
|
-
const it = intent ?? "";
|
|
186
|
-
const tpl = matchTemplate(it);
|
|
414
|
+
const it = (intent ?? "").trim();
|
|
187
415
|
const project = slugify(basename(process.cwd()));
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
nextSteps(["muxo validate", "muxo plan", "muxo apply", "muxo run <workflow> # or wait for the cron schedule", "muxo logs # watch executions"], opts);
|
|
192
|
-
return 0;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// src/lib/config.ts
|
|
196
|
-
import { readFile as readFile2 } from "node:fs/promises";
|
|
197
|
-
|
|
198
|
-
// src/lib/keychain.ts
|
|
199
|
-
import { execFile } from "node:child_process";
|
|
200
|
-
import { chmod, mkdir, readFile, unlink, writeFile as writeFile2 } from "node:fs/promises";
|
|
201
|
-
import { homedir } from "node:os";
|
|
202
|
-
import { dirname, join } from "node:path";
|
|
203
|
-
import { promisify } from "node:util";
|
|
204
|
-
var run = promisify(execFile);
|
|
205
|
-
var SERVICE = "muxo";
|
|
206
|
-
var ACCOUNT = "muxo-key";
|
|
207
|
-
var KEY_FILE = join(homedir(), ".muxo", "key");
|
|
208
|
-
async function keychainGet() {
|
|
209
|
-
if (process.platform === "darwin") {
|
|
416
|
+
let text;
|
|
417
|
+
let source = "template";
|
|
418
|
+
if (it !== "") {
|
|
210
419
|
try {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
ACCOUNT,
|
|
217
|
-
"-w"
|
|
218
|
-
]);
|
|
219
|
-
const key = stdout.trim();
|
|
220
|
-
if (key) return key;
|
|
420
|
+
const designed = await architectManifest(it, project, opts);
|
|
421
|
+
if (designed !== void 0) {
|
|
422
|
+
text = designed.yaml;
|
|
423
|
+
source = designed.source;
|
|
424
|
+
}
|
|
221
425
|
} catch {
|
|
222
426
|
}
|
|
223
427
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}
|
|
228
|
-
|
|
428
|
+
const tpl = matchTemplate(it);
|
|
429
|
+
if (text === void 0) text = renderManifest(project, it, tpl);
|
|
430
|
+
await writeFile2("muxo.yaml", text, "utf8");
|
|
431
|
+
info(`wrote muxo.yaml${source === "architect" ? " (designed by the muxo architect)" : ""}`, opts);
|
|
432
|
+
const parsed = validateManifest(text);
|
|
433
|
+
const bundles = parsed.ok && parsed.manifest !== void 0 ? Object.keys(parsed.manifest.bundles ?? {}) : Object.keys(tpl.bundles);
|
|
434
|
+
if (bundles.length > 0) {
|
|
435
|
+
info(`bundles: ${bundles.join(", ")}`, opts);
|
|
436
|
+
} else {
|
|
437
|
+
info("no bundles yet \u2014 add one with `muxo add <bundle>`", opts);
|
|
229
438
|
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
"-s",
|
|
238
|
-
SERVICE,
|
|
239
|
-
"-a",
|
|
240
|
-
ACCOUNT,
|
|
241
|
-
"-w",
|
|
242
|
-
key
|
|
243
|
-
]);
|
|
244
|
-
return "keychain";
|
|
245
|
-
} catch {
|
|
246
|
-
}
|
|
439
|
+
const hasDeploy = parsed.ok && parsed.manifest !== void 0 ? Object.values(parsed.manifest.bundles ?? {}).some((b) => b.capabilities.includes("deploy.site")) : tpl.bundles.deploy !== void 0;
|
|
440
|
+
let scaffolded = false;
|
|
441
|
+
if (hasDeploy && !await exists("public/index.html")) {
|
|
442
|
+
await mkdir2("public", { recursive: true });
|
|
443
|
+
await writeFile2("public/index.html", landingPage(project), "utf8");
|
|
444
|
+
info("wrote public/index.html", opts);
|
|
445
|
+
scaffolded = true;
|
|
247
446
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
return "file";
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// src/lib/config.ts
|
|
256
|
-
var DEFAULT_BASE_URL = "https://api.muxo.ai/v1";
|
|
257
|
-
var LOCAL_BASE_URL = "http://localhost:8787/v1";
|
|
258
|
-
function baseUrl(local) {
|
|
259
|
-
if (local) return LOCAL_BASE_URL;
|
|
260
|
-
return process.env.MUXO_API_BASE || DEFAULT_BASE_URL;
|
|
261
|
-
}
|
|
262
|
-
async function resolveKey() {
|
|
263
|
-
if (process.env.MUXO_KEY) return process.env.MUXO_KEY;
|
|
264
|
-
return keychainGet();
|
|
265
|
-
}
|
|
266
|
-
var MANIFEST_FILES = ["muxo.yaml", "muxo.muxo", ".muxo/muxo.yaml"];
|
|
267
|
-
async function readManifestText() {
|
|
268
|
-
for (const file of MANIFEST_FILES) {
|
|
269
|
-
try {
|
|
270
|
-
return await readFile2(file, "utf8");
|
|
271
|
-
} catch {
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
447
|
+
const steps = ["muxo validate", "muxo plan", "muxo apply"];
|
|
448
|
+
const workflowNames = parsed.ok && parsed.manifest !== void 0 ? Object.keys(parsed.manifest.workflows ?? {}) : Object.keys(tpl.workflows ?? {});
|
|
449
|
+
if (workflowNames.length > 0) {
|
|
450
|
+
steps.push(`muxo run ${workflowNames[0]}`, "muxo logs # watch executions");
|
|
274
451
|
}
|
|
275
|
-
|
|
452
|
+
if (hasDeploy) {
|
|
453
|
+
steps.push("muxo keys create # a key scoped to this manifest");
|
|
454
|
+
steps.push(`muxo deploy ${scaffolded ? "public" : "<dir>"} # upload the site \u2192 live URL`);
|
|
455
|
+
} else if (workflowNames.length === 0) {
|
|
456
|
+
steps.push("muxo status # confirm the applied state");
|
|
457
|
+
}
|
|
458
|
+
nextSteps(steps, opts);
|
|
459
|
+
return 0;
|
|
276
460
|
}
|
|
277
461
|
|
|
278
462
|
// src/commands/validate.ts
|
|
@@ -330,7 +514,7 @@ var manifestSchema = z.object({
|
|
|
330
514
|
}).optional(),
|
|
331
515
|
workflows: z.record(workflow).optional()
|
|
332
516
|
}).strict();
|
|
333
|
-
function
|
|
517
|
+
function validateManifest2(text) {
|
|
334
518
|
let doc;
|
|
335
519
|
try {
|
|
336
520
|
doc = parseYaml(text);
|
|
@@ -381,20 +565,32 @@ function validateManifest(text) {
|
|
|
381
565
|
}
|
|
382
566
|
|
|
383
567
|
// src/commands/validate.ts
|
|
568
|
+
var ZERO_USAGE = { credits: 0, provider: "muxo", latency_ms: 0 };
|
|
384
569
|
async function validate(opts) {
|
|
385
570
|
const text = await readManifestText();
|
|
386
571
|
if (text === void 0) {
|
|
387
|
-
const
|
|
388
|
-
if (opts.json)
|
|
389
|
-
|
|
572
|
+
const message = "no muxo.yaml found in this directory";
|
|
573
|
+
if (opts.json) {
|
|
574
|
+
printJson({ ok: false, error: { code: "not_found", message } });
|
|
575
|
+
} else {
|
|
576
|
+
console.error(`error: ${message}`);
|
|
577
|
+
}
|
|
390
578
|
return 1;
|
|
391
579
|
}
|
|
392
580
|
const issues = [
|
|
393
581
|
...coreValidate(text).errors.map((e) => ({ path: e.path, message: e.message + (e.hint ? ` (${e.hint})` : "") })),
|
|
394
|
-
...
|
|
582
|
+
...validateManifest2(text)
|
|
395
583
|
];
|
|
396
584
|
if (opts.json) {
|
|
397
|
-
|
|
585
|
+
if (issues.length === 0) {
|
|
586
|
+
printJson({ ok: true, data: { valid: true, issues: [] }, usage: ZERO_USAGE });
|
|
587
|
+
} else {
|
|
588
|
+
printJson({
|
|
589
|
+
ok: false,
|
|
590
|
+
error: { code: "invalid_manifest", message: `muxo.yaml is invalid (${issues.length} issue${issues.length === 1 ? "" : "s"})` },
|
|
591
|
+
data: { valid: false, issues }
|
|
592
|
+
});
|
|
593
|
+
}
|
|
398
594
|
} else if (issues.length === 0) {
|
|
399
595
|
if (!opts.quiet) console.log("muxo.yaml is valid");
|
|
400
596
|
} else {
|
|
@@ -405,49 +601,6 @@ async function validate(opts) {
|
|
|
405
601
|
return issues.length > 0 ? 1 : 0;
|
|
406
602
|
}
|
|
407
603
|
|
|
408
|
-
// src/lib/api.ts
|
|
409
|
-
async function request(opts, method, path, body) {
|
|
410
|
-
if (opts.debug) {
|
|
411
|
-
console.error(`muxo: ${method} ${opts.baseUrl}${path}`);
|
|
412
|
-
}
|
|
413
|
-
let res;
|
|
414
|
-
try {
|
|
415
|
-
res = await fetch(`${opts.baseUrl}${path}`, {
|
|
416
|
-
method,
|
|
417
|
-
headers: {
|
|
418
|
-
...opts.key ? { authorization: `Bearer ${opts.key}` } : {},
|
|
419
|
-
"content-type": "application/json"
|
|
420
|
-
},
|
|
421
|
-
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
422
|
-
});
|
|
423
|
-
} catch (err) {
|
|
424
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
425
|
-
return {
|
|
426
|
-
ok: false,
|
|
427
|
-
error: {
|
|
428
|
-
code: "internal",
|
|
429
|
-
message: `network error reaching ${opts.baseUrl}${path}: ${reason}. If this is your first run, set MUXO_API_BASE to your runtime URL (e.g. export MUXO_API_BASE=https://api.muxo.ai/v1)`
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
}
|
|
433
|
-
try {
|
|
434
|
-
return await res.json();
|
|
435
|
-
} catch {
|
|
436
|
-
return {
|
|
437
|
-
ok: false,
|
|
438
|
-
error: { code: "internal", message: `muxo returned HTTP ${res.status} with a non-JSON body` }
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
function makeApi(opts) {
|
|
443
|
-
return {
|
|
444
|
-
get: (path) => request(opts, "GET", path),
|
|
445
|
-
post: (path, body) => request(opts, "POST", path, body),
|
|
446
|
-
put: (path, body) => request(opts, "PUT", path, body),
|
|
447
|
-
del: (path) => request(opts, "DELETE", path)
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
|
|
451
604
|
// src/commands/plan.ts
|
|
452
605
|
async function plan(opts) {
|
|
453
606
|
const text = await readManifestText();
|
|
@@ -456,15 +609,15 @@ async function plan(opts) {
|
|
|
456
609
|
return 1;
|
|
457
610
|
}
|
|
458
611
|
const api = makeApi({
|
|
459
|
-
baseUrl: baseUrl(
|
|
612
|
+
baseUrl: baseUrl(),
|
|
460
613
|
key: await resolveKey(),
|
|
461
614
|
debug: opts.debug
|
|
462
615
|
});
|
|
463
616
|
const env = await api.post("/plan", { yaml: text });
|
|
464
|
-
return renderEnvelope(env, !!opts.json, (
|
|
465
|
-
const
|
|
466
|
-
if (Array.isArray(
|
|
467
|
-
if (
|
|
617
|
+
return renderEnvelope(env, !!opts.json, (data2) => {
|
|
618
|
+
const ops2 = data2?.ops;
|
|
619
|
+
if (Array.isArray(ops2)) {
|
|
620
|
+
if (ops2.length === 0) {
|
|
468
621
|
console.log("no changes \u2014 manifest matches the deployed state");
|
|
469
622
|
return;
|
|
470
623
|
}
|
|
@@ -477,16 +630,16 @@ async function plan(opts) {
|
|
|
477
630
|
deprovision: "-",
|
|
478
631
|
noop: "!"
|
|
479
632
|
};
|
|
480
|
-
|
|
633
|
+
ops2.forEach((op, i) => {
|
|
481
634
|
const icon = ICONS[op.kind] ?? "\xB7";
|
|
482
635
|
const label = op.detail !== void 0 && op.detail !== "" ? `${op.ref}: ${op.detail}` : op.ref;
|
|
483
636
|
console.log(`${i + 1}. ${icon} ${label}`);
|
|
484
637
|
});
|
|
485
|
-
const removes =
|
|
638
|
+
const removes = ops2.filter((o) => o.kind === "deprovision").length;
|
|
486
639
|
if (removes > 0) console.log(`
|
|
487
640
|
\u26A0 ${removes} deprovision op(s) \u2014 apply will remove these resources`);
|
|
488
641
|
} else {
|
|
489
|
-
console.log(JSON.stringify(
|
|
642
|
+
console.log(JSON.stringify(data2, null, 2));
|
|
490
643
|
}
|
|
491
644
|
});
|
|
492
645
|
}
|
|
@@ -497,16 +650,16 @@ async function apply(opts) {
|
|
|
497
650
|
return 1;
|
|
498
651
|
}
|
|
499
652
|
const api = makeApi({
|
|
500
|
-
baseUrl: baseUrl(
|
|
653
|
+
baseUrl: baseUrl(),
|
|
501
654
|
key: await resolveKey(),
|
|
502
655
|
debug: opts.debug
|
|
503
656
|
});
|
|
504
657
|
const env = await api.put("/manifest", { yaml: text });
|
|
505
|
-
return renderEnvelope(env, !!opts.json, (
|
|
506
|
-
const d =
|
|
658
|
+
return renderEnvelope(env, !!opts.json, (data2) => {
|
|
659
|
+
const d = data2;
|
|
507
660
|
const version = d?.version !== void 0 ? ` version ${d.version}` : "";
|
|
508
|
-
const
|
|
509
|
-
console.log(`applied${version}${
|
|
661
|
+
const ops2 = Array.isArray(d?.ops) ? ` (${d.ops.length} ops)` : "";
|
|
662
|
+
console.log(`applied${version}${ops2}`);
|
|
510
663
|
if (Array.isArray(d?.ops)) {
|
|
511
664
|
d.ops.forEach((op, i) => {
|
|
512
665
|
const label = typeof op === "string" ? op : JSON.stringify(op);
|
|
@@ -559,21 +712,7 @@ async function add(bundleName, opts) {
|
|
|
559
712
|
return 0;
|
|
560
713
|
}
|
|
561
714
|
|
|
562
|
-
// src/
|
|
563
|
-
function printSteps(data) {
|
|
564
|
-
const d = data;
|
|
565
|
-
if (d?.execution_id !== void 0) console.log(`execution ${String(d.execution_id)}`);
|
|
566
|
-
if (Array.isArray(d?.steps)) {
|
|
567
|
-
for (const step of d.steps) {
|
|
568
|
-
const name = typeof step?.name === "string" ? step.name : "step";
|
|
569
|
-
const status2 = typeof step?.status === "string" ? step.status : JSON.stringify(step);
|
|
570
|
-
console.log(` ${name}: ${status2}`);
|
|
571
|
-
if (step?.error !== void 0) console.log(` ${JSON.stringify(step.error)}`);
|
|
572
|
-
}
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
console.log(JSON.stringify(data, null, 2));
|
|
576
|
-
}
|
|
715
|
+
// src/lib/params.ts
|
|
577
716
|
function coerceParam(value) {
|
|
578
717
|
if (value === "true") return true;
|
|
579
718
|
if (value === "false") return false;
|
|
@@ -589,9 +728,125 @@ function parseParams(raw) {
|
|
|
589
728
|
}
|
|
590
729
|
return params;
|
|
591
730
|
}
|
|
731
|
+
|
|
732
|
+
// src/commands/call.ts
|
|
733
|
+
async function call(capability, opts) {
|
|
734
|
+
let input = {};
|
|
735
|
+
if (opts.data !== void 0) {
|
|
736
|
+
try {
|
|
737
|
+
const parsed = JSON.parse(opts.data);
|
|
738
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
739
|
+
console.error("error: --data must be a JSON object");
|
|
740
|
+
return 1;
|
|
741
|
+
}
|
|
742
|
+
input = parsed;
|
|
743
|
+
} catch (e) {
|
|
744
|
+
console.error(`error: --data is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
745
|
+
return 1;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
try {
|
|
749
|
+
input = { ...input, ...parseParams(opts.param) };
|
|
750
|
+
} catch (e) {
|
|
751
|
+
console.error(`error: ${e instanceof Error ? e.message : String(e)}`);
|
|
752
|
+
return 1;
|
|
753
|
+
}
|
|
754
|
+
const api = makeApi({
|
|
755
|
+
baseUrl: baseUrl(),
|
|
756
|
+
key: await resolveKey(),
|
|
757
|
+
debug: opts.debug
|
|
758
|
+
});
|
|
759
|
+
const env = await api.post(`/capabilities/${capability}`, input);
|
|
760
|
+
return renderEnvelope(env, !!opts.json, (data2) => printJson(data2));
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/commands/deploy.ts
|
|
764
|
+
import { access as access2, readdir, readFile as readFile4 } from "node:fs/promises";
|
|
765
|
+
import { join as join2, relative, sep } from "node:path";
|
|
766
|
+
async function exists2(path) {
|
|
767
|
+
try {
|
|
768
|
+
await access2(path);
|
|
769
|
+
return true;
|
|
770
|
+
} catch {
|
|
771
|
+
return false;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async function collectFiles(dir) {
|
|
775
|
+
const out = {};
|
|
776
|
+
async function walk(current) {
|
|
777
|
+
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
778
|
+
if (entry.name.startsWith(".")) continue;
|
|
779
|
+
const full = join2(current, entry.name);
|
|
780
|
+
if (entry.isDirectory()) {
|
|
781
|
+
await walk(full);
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
if (!entry.isFile()) continue;
|
|
785
|
+
const rel = relative(dir, full).split(sep).join("/");
|
|
786
|
+
out[`/${rel}`] = await readFile4(full, "utf8");
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
await walk(dir);
|
|
790
|
+
return out;
|
|
791
|
+
}
|
|
792
|
+
async function deploy(dirArg, opts) {
|
|
793
|
+
let dir = dirArg;
|
|
794
|
+
if (dir === void 0) {
|
|
795
|
+
for (const candidate of ["public", "dist", "build", "out"]) {
|
|
796
|
+
if (await exists2(candidate)) {
|
|
797
|
+
dir = candidate;
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (dir === void 0 || !await exists2(dir)) {
|
|
803
|
+
console.error(`error: no site directory found${dir !== void 0 ? ` at "${dir}"` : ""} \u2014 pass one: muxo deploy <dir>`);
|
|
804
|
+
return 1;
|
|
805
|
+
}
|
|
806
|
+
const files = await collectFiles(dir);
|
|
807
|
+
const count = Object.keys(files).length;
|
|
808
|
+
if (count === 0) {
|
|
809
|
+
console.error(`error: "${dir}" is empty`);
|
|
810
|
+
return 1;
|
|
811
|
+
}
|
|
812
|
+
if (!opts.quiet && !opts.json) console.error(`deploying ${count} file(s) from ${dir}\u2026`);
|
|
813
|
+
const api = makeApi({
|
|
814
|
+
baseUrl: baseUrl(),
|
|
815
|
+
key: await resolveKey(),
|
|
816
|
+
debug: opts.debug
|
|
817
|
+
});
|
|
818
|
+
const env = await api.post("/capabilities/deploy.site", {
|
|
819
|
+
files,
|
|
820
|
+
...opts.target !== void 0 ? { target: opts.target } : {}
|
|
821
|
+
});
|
|
822
|
+
return renderEnvelope(env, !!opts.json, (data2) => {
|
|
823
|
+
const d = data2;
|
|
824
|
+
if (typeof d?.url === "string" && d.url !== "") {
|
|
825
|
+
console.log(`deployed: ${d.url}`);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
console.log(JSON.stringify(data2, null, 2));
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
// src/commands/run.ts
|
|
833
|
+
function printSteps(data2) {
|
|
834
|
+
const d = data2;
|
|
835
|
+
if (d?.execution_id !== void 0) console.log(`execution ${String(d.execution_id)}`);
|
|
836
|
+
if (Array.isArray(d?.steps)) {
|
|
837
|
+
for (const step of d.steps) {
|
|
838
|
+
const name = typeof step?.name === "string" ? step.name : "step";
|
|
839
|
+
const status2 = typeof step?.status === "string" ? step.status : JSON.stringify(step);
|
|
840
|
+
console.log(` ${name}: ${status2}`);
|
|
841
|
+
if (step?.error !== void 0) console.log(` ${JSON.stringify(step.error)}`);
|
|
842
|
+
}
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
console.log(JSON.stringify(data2, null, 2));
|
|
846
|
+
}
|
|
592
847
|
async function run2(workflow2, opts) {
|
|
593
848
|
const api = makeApi({
|
|
594
|
-
baseUrl: baseUrl(
|
|
849
|
+
baseUrl: baseUrl(),
|
|
595
850
|
key: await resolveKey(),
|
|
596
851
|
debug: opts.debug
|
|
597
852
|
});
|
|
@@ -621,13 +876,13 @@ async function run2(workflow2, opts) {
|
|
|
621
876
|
}
|
|
622
877
|
async function status(opts) {
|
|
623
878
|
const api = makeApi({
|
|
624
|
-
baseUrl: baseUrl(
|
|
879
|
+
baseUrl: baseUrl(),
|
|
625
880
|
key: await resolveKey(),
|
|
626
881
|
debug: opts.debug
|
|
627
882
|
});
|
|
628
883
|
const env = await api.get("/status");
|
|
629
|
-
return renderEnvelope(env, !!opts.json, (
|
|
630
|
-
const d =
|
|
884
|
+
return renderEnvelope(env, !!opts.json, (data2) => {
|
|
885
|
+
const d = data2;
|
|
631
886
|
if (d && typeof d === "object" && !Array.isArray(d)) {
|
|
632
887
|
if (d.project !== void 0) console.log(`project: ${String(d.project)}`);
|
|
633
888
|
if (d.version !== void 0) console.log(`version: ${String(d.version)}`);
|
|
@@ -640,31 +895,31 @@ async function status(opts) {
|
|
|
640
895
|
if (d.drift !== void 0) console.log(`drift: ${JSON.stringify(d.drift)}`);
|
|
641
896
|
return;
|
|
642
897
|
}
|
|
643
|
-
console.log(JSON.stringify(
|
|
898
|
+
console.log(JSON.stringify(data2, null, 2));
|
|
644
899
|
});
|
|
645
900
|
}
|
|
646
901
|
async function rollback(version, opts) {
|
|
647
902
|
const api = makeApi({
|
|
648
|
-
baseUrl: baseUrl(
|
|
903
|
+
baseUrl: baseUrl(),
|
|
649
904
|
key: await resolveKey(),
|
|
650
905
|
debug: opts.debug
|
|
651
906
|
});
|
|
652
907
|
const body = version === void 0 ? {} : { version: Number(version) };
|
|
653
908
|
const env = await api.post("/rollback", body);
|
|
654
|
-
return renderEnvelope(env, !!opts.json, (
|
|
655
|
-
console.log(`rolled back to ${JSON.stringify(
|
|
909
|
+
return renderEnvelope(env, !!opts.json, (data2) => {
|
|
910
|
+
console.log(`rolled back to ${JSON.stringify(data2)}`);
|
|
656
911
|
});
|
|
657
912
|
}
|
|
658
913
|
async function logs(workflow2, opts) {
|
|
659
914
|
const api = makeApi({
|
|
660
|
-
baseUrl: baseUrl(
|
|
915
|
+
baseUrl: baseUrl(),
|
|
661
916
|
key: await resolveKey(),
|
|
662
917
|
debug: opts.debug
|
|
663
918
|
});
|
|
664
919
|
const qs = workflow2 ? `?workflow=${encodeURIComponent(workflow2)}` : "";
|
|
665
920
|
const env = await api.get(`/workflows/${encodeURIComponent(workflow2 ?? "_all")}/executions${qs}`);
|
|
666
|
-
return renderEnvelope(env, !!opts.json, (
|
|
667
|
-
const d =
|
|
921
|
+
return renderEnvelope(env, !!opts.json, (data2) => {
|
|
922
|
+
const d = data2;
|
|
668
923
|
if (Array.isArray(d?.executions)) {
|
|
669
924
|
for (const e of d.executions) {
|
|
670
925
|
const when = new Date(e.started_at).toISOString().slice(0, 19).replace("T", " ");
|
|
@@ -674,18 +929,18 @@ async function logs(workflow2, opts) {
|
|
|
674
929
|
}
|
|
675
930
|
return;
|
|
676
931
|
}
|
|
677
|
-
console.log(JSON.stringify(
|
|
932
|
+
console.log(JSON.stringify(data2, null, 2));
|
|
678
933
|
});
|
|
679
934
|
}
|
|
680
935
|
async function stepLogs(instanceId, opts) {
|
|
681
936
|
const api = makeApi({
|
|
682
|
-
baseUrl: baseUrl(
|
|
937
|
+
baseUrl: baseUrl(),
|
|
683
938
|
key: await resolveKey(),
|
|
684
939
|
debug: opts.debug
|
|
685
940
|
});
|
|
686
941
|
const env = await api.get(`/executions/${encodeURIComponent(instanceId)}/steps`);
|
|
687
|
-
return renderEnvelope(env, !!opts.json, (
|
|
688
|
-
const rows =
|
|
942
|
+
return renderEnvelope(env, !!opts.json, (data2) => {
|
|
943
|
+
const rows = data2?.steps ?? [];
|
|
689
944
|
for (const s of rows) {
|
|
690
945
|
const mark = s.status === "ok" ? "\u2713" : "\u2717";
|
|
691
946
|
const err = s.error ? ` \u2014 ${s.error}` : "";
|
|
@@ -695,10 +950,25 @@ async function stepLogs(instanceId, opts) {
|
|
|
695
950
|
}
|
|
696
951
|
|
|
697
952
|
// src/commands/keys.ts
|
|
953
|
+
import { parseManifest } from "@muxoai/core";
|
|
954
|
+
async function bootstrapBody() {
|
|
955
|
+
const text = await readManifestText();
|
|
956
|
+
if (text === void 0) return {};
|
|
957
|
+
const parsed = parseManifest(text);
|
|
958
|
+
if (!parsed.ok || parsed.manifest === void 0) return {};
|
|
959
|
+
const capabilities2 = [
|
|
960
|
+
...new Set(Object.values(parsed.manifest.bundles ?? {}).flatMap((b) => b.capabilities ?? []))
|
|
961
|
+
];
|
|
962
|
+
return {
|
|
963
|
+
project: parsed.manifest.project,
|
|
964
|
+
...capabilities2.length > 0 ? { scopes: capabilities2 } : {}
|
|
965
|
+
};
|
|
966
|
+
}
|
|
698
967
|
async function keysCommand(action, id, opts) {
|
|
968
|
+
const existingKey = await resolveKey();
|
|
699
969
|
const api = makeApi({
|
|
700
|
-
baseUrl: baseUrl(
|
|
701
|
-
key:
|
|
970
|
+
baseUrl: baseUrl(),
|
|
971
|
+
key: existingKey,
|
|
702
972
|
debug: opts.debug
|
|
703
973
|
});
|
|
704
974
|
if (action === "revoke" && !id) {
|
|
@@ -722,7 +992,7 @@ async function keysCommand(action, id, opts) {
|
|
|
722
992
|
env = await api.get("/keys");
|
|
723
993
|
break;
|
|
724
994
|
case "create":
|
|
725
|
-
env = await api.post("/keys");
|
|
995
|
+
env = await api.post("/keys", existingKey === void 0 ? await bootstrapBody() : void 0);
|
|
726
996
|
break;
|
|
727
997
|
case "rotate":
|
|
728
998
|
env = await api.post(`/keys/${id}/rotate`);
|
|
@@ -731,32 +1001,61 @@ async function keysCommand(action, id, opts) {
|
|
|
731
1001
|
env = await api.del(`/keys/${id}`);
|
|
732
1002
|
break;
|
|
733
1003
|
}
|
|
1004
|
+
let storedIn;
|
|
1005
|
+
if (env.ok && action === "create" && existingKey === void 0) {
|
|
1006
|
+
const data2 = env.data;
|
|
1007
|
+
if (typeof data2?.key === "string") {
|
|
1008
|
+
storedIn = await keychainSet(data2.key);
|
|
1009
|
+
if (!opts.json && storedIn === "file") {
|
|
1010
|
+
console.error("warning: keychain unavailable; key stored in ~/.muxo/key (chmod 600)");
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
734
1014
|
if (env.ok && (action === "create" || action === "rotate") && !opts.json) {
|
|
735
|
-
const
|
|
736
|
-
if (
|
|
737
|
-
const
|
|
738
|
-
console.log(`${action === "rotate" ? "rotated" : "created"} key ${
|
|
739
|
-
console.log(String(
|
|
740
|
-
|
|
1015
|
+
const data2 = env.data;
|
|
1016
|
+
if (data2?.key !== void 0) {
|
|
1017
|
+
const keyId = data2.keyId ?? data2.key_id ?? data2.id;
|
|
1018
|
+
console.log(`${action === "rotate" ? "rotated" : "created"} key ${keyId !== void 0 ? String(keyId) : ""}`);
|
|
1019
|
+
console.log(String(data2.key));
|
|
1020
|
+
if (data2.project_id !== void 0) {
|
|
1021
|
+
console.log(`project ${String(data2.project_id)}${data2.credits !== void 0 ? ` \xB7 ${String(data2.credits)} credits` : ""}`);
|
|
1022
|
+
}
|
|
1023
|
+
console.log(
|
|
1024
|
+
storedIn !== void 0 ? `stored in ${storedIn === "keychain" ? "OS keychain" : "~/.muxo/key"} \u2014 ready to use` : "store it now \u2014 it will not be shown again"
|
|
1025
|
+
);
|
|
741
1026
|
return 0;
|
|
742
1027
|
}
|
|
743
1028
|
}
|
|
744
|
-
return renderEnvelope(env, !!opts.json, (
|
|
745
|
-
if (Array.isArray(
|
|
746
|
-
for (const key of
|
|
1029
|
+
return renderEnvelope(env, !!opts.json, (data2) => {
|
|
1030
|
+
if (Array.isArray(data2)) {
|
|
1031
|
+
for (const key of data2) {
|
|
747
1032
|
const k = key;
|
|
748
1033
|
console.log(`${String(k?.id ?? "?")}${k?.revoked ? " (revoked)" : ""}`);
|
|
749
1034
|
}
|
|
750
1035
|
return;
|
|
751
1036
|
}
|
|
752
|
-
printJson(
|
|
1037
|
+
printJson(data2);
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
// src/commands/capabilities.ts
|
|
1042
|
+
async function capabilities(opts) {
|
|
1043
|
+
const api = makeApi({ baseUrl: baseUrl(), key: await resolveKey(), debug: opts.debug });
|
|
1044
|
+
const env = await api.get("/capabilities");
|
|
1045
|
+
return renderEnvelope(env, !!opts.json, (data2) => {
|
|
1046
|
+
const rows = data2.capabilities ?? [];
|
|
1047
|
+
for (const c of rows) {
|
|
1048
|
+
const providers = c.backends.map((b) => b.configured ? b.provider : `${b.provider}*`).join(", ");
|
|
1049
|
+
console.log(`${c.available ? "ok " : "-- "}${c.name.padEnd(20)} ${providers}`);
|
|
1050
|
+
}
|
|
1051
|
+
console.log("\n* = not configured (missing runtime secrets)");
|
|
753
1052
|
});
|
|
754
1053
|
}
|
|
755
1054
|
|
|
756
1055
|
// src/commands/credits.ts
|
|
757
1056
|
async function creditsCommand(action, packId, opts) {
|
|
758
1057
|
const api = makeApi({
|
|
759
|
-
baseUrl: baseUrl(
|
|
1058
|
+
baseUrl: baseUrl(),
|
|
760
1059
|
key: await resolveKey(),
|
|
761
1060
|
debug: opts.debug
|
|
762
1061
|
});
|
|
@@ -766,8 +1065,8 @@ async function creditsCommand(action, packId, opts) {
|
|
|
766
1065
|
return 1;
|
|
767
1066
|
}
|
|
768
1067
|
const env2 = await api.post("/credits/checkout", { pack: packId });
|
|
769
|
-
return renderEnvelope(env2, !!opts.json, (
|
|
770
|
-
const d =
|
|
1068
|
+
return renderEnvelope(env2, !!opts.json, (data2) => {
|
|
1069
|
+
const d = data2;
|
|
771
1070
|
if (d?.url !== void 0) {
|
|
772
1071
|
console.log(`checkout: ${d.url}`);
|
|
773
1072
|
if (d.stubbed) console.log("(stub \u2014 set STRIPE_SECRET_KEY on the runtime for real checkout)");
|
|
@@ -775,8 +1074,8 @@ async function creditsCommand(action, packId, opts) {
|
|
|
775
1074
|
});
|
|
776
1075
|
}
|
|
777
1076
|
const env = await api.get("/credits");
|
|
778
|
-
return renderEnvelope(env, !!opts.json, (
|
|
779
|
-
const d =
|
|
1077
|
+
return renderEnvelope(env, !!opts.json, (data2) => {
|
|
1078
|
+
const d = data2;
|
|
780
1079
|
if (action === "packs") {
|
|
781
1080
|
for (const p of d?.packs ?? []) {
|
|
782
1081
|
console.log(`${p.id} ${p.credits} credits $${(p.price_cents / 100).toFixed(2)}`);
|
|
@@ -807,7 +1106,7 @@ function promptHidden(promptText) {
|
|
|
807
1106
|
});
|
|
808
1107
|
return;
|
|
809
1108
|
}
|
|
810
|
-
process.
|
|
1109
|
+
process.stderr.write(promptText);
|
|
811
1110
|
const wasRaw = process.stdin.isRaw ?? false;
|
|
812
1111
|
process.stdin.setRawMode(true);
|
|
813
1112
|
process.stdin.resume();
|
|
@@ -818,7 +1117,7 @@ function promptHidden(promptText) {
|
|
|
818
1117
|
process.stdin.setRawMode(wasRaw);
|
|
819
1118
|
process.stdin.pause();
|
|
820
1119
|
process.stdin.removeListener("data", onData);
|
|
821
|
-
process.
|
|
1120
|
+
process.stderr.write("\n");
|
|
822
1121
|
resolve(buf);
|
|
823
1122
|
return;
|
|
824
1123
|
}
|
|
@@ -828,7 +1127,7 @@ function promptHidden(promptText) {
|
|
|
828
1127
|
process.stdin.setRawMode(wasRaw);
|
|
829
1128
|
process.stdin.pause();
|
|
830
1129
|
process.stdin.removeListener("data", onData);
|
|
831
|
-
process.
|
|
1130
|
+
process.stderr.write("\n");
|
|
832
1131
|
process.exit(130);
|
|
833
1132
|
} else {
|
|
834
1133
|
buf += ch;
|
|
@@ -841,14 +1140,41 @@ function promptHidden(promptText) {
|
|
|
841
1140
|
async function login(opts) {
|
|
842
1141
|
const key = await promptHidden("Paste your muxo key: ");
|
|
843
1142
|
if (!key) {
|
|
844
|
-
|
|
1143
|
+
if (opts.json) {
|
|
1144
|
+
printJson({ ok: false, error: { code: "invalid_input", message: "no key provided" } });
|
|
1145
|
+
} else {
|
|
1146
|
+
console.error("error: no key provided");
|
|
1147
|
+
}
|
|
845
1148
|
return 1;
|
|
846
1149
|
}
|
|
1150
|
+
const api = makeApi({ baseUrl: baseUrl(), key, debug: opts.debug });
|
|
1151
|
+
const check = await api.get("/status");
|
|
1152
|
+
if (!check.ok && check.error.code === "forbidden") {
|
|
1153
|
+
if (opts.json) {
|
|
1154
|
+
printJson(check);
|
|
1155
|
+
} else {
|
|
1156
|
+
console.error(`error: ${check.error.message}`);
|
|
1157
|
+
}
|
|
1158
|
+
return 2;
|
|
1159
|
+
}
|
|
847
1160
|
const where = await keychainSet(key);
|
|
848
1161
|
if (where === "file") {
|
|
849
1162
|
console.error("warning: keychain unavailable; key stored in ~/.muxo/key (chmod 600)");
|
|
850
1163
|
}
|
|
1164
|
+
if (opts.json) {
|
|
1165
|
+
printJson({
|
|
1166
|
+
ok: true,
|
|
1167
|
+
data: { stored: where, validated: check.ok },
|
|
1168
|
+
usage: { credits: 0, provider: "muxo", latency_ms: 0 }
|
|
1169
|
+
});
|
|
1170
|
+
return 0;
|
|
1171
|
+
}
|
|
851
1172
|
info(where === "keychain" ? "key stored in OS keychain" : "key stored in ~/.muxo/key", opts);
|
|
1173
|
+
if (!check.ok) {
|
|
1174
|
+
console.error(`warning: could not verify key against the runtime (${check.error.message}) \u2014 stored anyway`);
|
|
1175
|
+
} else {
|
|
1176
|
+
info("key verified against the runtime", opts);
|
|
1177
|
+
}
|
|
852
1178
|
nextSteps(["muxo status"], opts);
|
|
853
1179
|
return 0;
|
|
854
1180
|
}
|
|
@@ -867,6 +1193,9 @@ var STATIC_TOOLS = [
|
|
|
867
1193
|
{ name: "llm_tts", description: "Text to speech. Inputs: text (required), voice?." },
|
|
868
1194
|
{ name: "compute_sandbox", description: "Run code in a sandbox. Inputs: code (required), runtime?, timeout?." },
|
|
869
1195
|
{ name: "kv_store", description: "Key-value store. Inputs: key (required), value (to write) or get (to read)." },
|
|
1196
|
+
{ name: "db_write", description: "Write rows to a stack database. Inputs: into (stack address, required), rows (required)." },
|
|
1197
|
+
{ name: "db_query", description: "Query a stack database. Inputs: sql (required)." },
|
|
1198
|
+
{ name: "dns_manage", description: "Manage DNS records for a domain. Inputs: domain (required), records." },
|
|
870
1199
|
{ name: "storage_object", description: "Object storage. Inputs: key (required), file (write) or get (read)." },
|
|
871
1200
|
{ name: "vector_search", description: "Vector search. Inputs: query (required), k?." },
|
|
872
1201
|
{ name: "email_send", description: "Send an email. Inputs: to, subject, body (all required)." },
|
|
@@ -889,10 +1218,10 @@ async function fetchTools(baseUrl2, key) {
|
|
|
889
1218
|
const api = makeApi({ baseUrl: baseUrl2, key });
|
|
890
1219
|
const env = await api.get("/status");
|
|
891
1220
|
if (!env.ok) return STATIC_TOOLS.map((t) => toolShape(t.name, t.description));
|
|
892
|
-
const
|
|
893
|
-
if (Array.isArray(
|
|
1221
|
+
const data2 = env.data;
|
|
1222
|
+
if (Array.isArray(data2?.tools)) {
|
|
894
1223
|
const tools = [];
|
|
895
|
-
for (const t of
|
|
1224
|
+
for (const t of data2.tools) {
|
|
896
1225
|
if (typeof t === "string") {
|
|
897
1226
|
tools.push(toolShape(toToolName(t), ""));
|
|
898
1227
|
continue;
|
|
@@ -908,15 +1237,15 @@ async function fetchTools(baseUrl2, key) {
|
|
|
908
1237
|
const filtered = tools.filter((t) => !excluded(t.name));
|
|
909
1238
|
return filtered.length > 0 ? filtered : STATIC_TOOLS.map((t) => toolShape(t.name, t.description));
|
|
910
1239
|
}
|
|
911
|
-
if (Array.isArray(
|
|
912
|
-
const tools =
|
|
1240
|
+
if (Array.isArray(data2?.capabilities)) {
|
|
1241
|
+
const tools = data2.capabilities.filter((c) => typeof c === "string").map((c) => toolShape(toToolName(c), "")).filter((t) => !excluded(t.name));
|
|
913
1242
|
return tools.length > 0 ? tools : STATIC_TOOLS.map((t) => toolShape(t.name, t.description));
|
|
914
1243
|
}
|
|
915
1244
|
return STATIC_TOOLS.map((t) => toolShape(t.name, t.description));
|
|
916
1245
|
}
|
|
917
|
-
function successResult(
|
|
1246
|
+
function successResult(data2, usage) {
|
|
918
1247
|
return {
|
|
919
|
-
content: [{ type: "text", text: JSON.stringify({ data, usage }) }],
|
|
1248
|
+
content: [{ type: "text", text: JSON.stringify({ data: data2, usage }) }],
|
|
920
1249
|
isError: false
|
|
921
1250
|
};
|
|
922
1251
|
}
|
|
@@ -931,7 +1260,7 @@ function errorResult(error) {
|
|
|
931
1260
|
};
|
|
932
1261
|
}
|
|
933
1262
|
async function mcp(opts) {
|
|
934
|
-
const baseUrl2 =
|
|
1263
|
+
const baseUrl2 = process.env.MUXO_API_BASE || DEFAULT_BASE_URL;
|
|
935
1264
|
const key = await resolveKey();
|
|
936
1265
|
const api = makeApi({ baseUrl: baseUrl2, key });
|
|
937
1266
|
let tools = null;
|
|
@@ -953,7 +1282,7 @@ async function mcp(opts) {
|
|
|
953
1282
|
reply(id, {
|
|
954
1283
|
protocolVersion: version,
|
|
955
1284
|
capabilities: { tools: { listChanged: false } },
|
|
956
|
-
serverInfo: { name: "muxo", version: "0.1.
|
|
1285
|
+
serverInfo: { name: "muxo", version: "0.1.1" }
|
|
957
1286
|
});
|
|
958
1287
|
return;
|
|
959
1288
|
}
|
|
@@ -1007,11 +1336,12 @@ function flags(cmd) {
|
|
|
1007
1336
|
}
|
|
1008
1337
|
async function main() {
|
|
1009
1338
|
const program = new Command();
|
|
1010
|
-
program.name("muxo").description("Muxo CLI \u2014 one key for your whole stack").version("0.1.
|
|
1339
|
+
program.name("muxo").description("Muxo CLI \u2014 one key for your whole stack").version("0.1.1").addHelpText("after", `
|
|
1011
1340
|
Examples:
|
|
1012
1341
|
muxo init "scrape hacker news every hour and store titles"
|
|
1013
1342
|
muxo validate && muxo plan && muxo apply
|
|
1014
1343
|
muxo run hn-scraper --param max_items=3
|
|
1344
|
+
muxo call web.search --data '{"query":"agent infra","limit":3}'
|
|
1015
1345
|
muxo logs && muxo steps <instanceId>
|
|
1016
1346
|
|
|
1017
1347
|
Auth: export MUXO_KEY=mk_... Endpoint: export MUXO_API_BASE=https://api.muxo.ai/v1
|
|
@@ -1047,11 +1377,29 @@ Docs: https://api.muxo.ai/docs/quickstart`);
|
|
|
1047
1377
|
runCmd.description("Execute a workflow one-shot").action(async (workflow2, opts) => {
|
|
1048
1378
|
process.exitCode = await run2(workflow2, opts);
|
|
1049
1379
|
});
|
|
1380
|
+
const callCmd = program.command("call <capability>");
|
|
1381
|
+
flags(callCmd);
|
|
1382
|
+
callCmd.option("--data <json>", `JSON object of capability inputs, e.g. --data '{"query":"x"}'`).option("--param <key=value>", "input field, e.g. --param limit=5 (repeatable)", (v, prev) => [...prev, v], []);
|
|
1383
|
+
callCmd.description("Invoke a capability directly (web.search, deploy.site, llm.chat, \u2026)").action(
|
|
1384
|
+
async (capability, opts) => {
|
|
1385
|
+
process.exitCode = await call(capability, opts);
|
|
1386
|
+
}
|
|
1387
|
+
);
|
|
1388
|
+
const deployCmd = program.command("deploy [dir]");
|
|
1389
|
+
flags(deployCmd);
|
|
1390
|
+
deployCmd.option("--target <target>", "deployment target, e.g. production").description("Deploy a local directory as a static site (default: public/ or dist/)").action(async (dir, opts) => {
|
|
1391
|
+
process.exitCode = await deploy(dir, opts);
|
|
1392
|
+
});
|
|
1050
1393
|
const statusCmd = program.command("status");
|
|
1051
1394
|
flags(statusCmd);
|
|
1052
1395
|
statusCmd.description("Project health, bundles, budget state").action(async (opts) => {
|
|
1053
1396
|
process.exitCode = await status(opts);
|
|
1054
1397
|
});
|
|
1398
|
+
const capabilitiesCmd = program.command("capabilities");
|
|
1399
|
+
flags(capabilitiesCmd);
|
|
1400
|
+
capabilitiesCmd.description("Show which capabilities this deployment can serve").action(async (opts) => {
|
|
1401
|
+
process.exitCode = await capabilities(opts);
|
|
1402
|
+
});
|
|
1055
1403
|
const keys = program.command("keys");
|
|
1056
1404
|
flags(keys);
|
|
1057
1405
|
keys.description("Manage project keys");
|
|
@@ -1110,13 +1458,12 @@ Docs: https://api.muxo.ai/docs/quickstart`);
|
|
|
1110
1458
|
});
|
|
1111
1459
|
const loginCmd = program.command("login");
|
|
1112
1460
|
flags(loginCmd);
|
|
1113
|
-
loginCmd.description("Store your muxo key in the OS keychain").action(async (opts) => {
|
|
1461
|
+
loginCmd.description("Store your muxo key in the OS keychain (validated against the runtime)").action(async (opts) => {
|
|
1114
1462
|
process.exitCode = await login(opts);
|
|
1115
1463
|
});
|
|
1116
1464
|
const mcpCmd = program.command("mcp");
|
|
1117
1465
|
flags(mcpCmd);
|
|
1118
|
-
mcpCmd.
|
|
1119
|
-
mcpCmd.description("Stdio MCP proxy wrapping the muxo API (SPEC \xA712)").action(async (opts) => {
|
|
1466
|
+
mcpCmd.description("Stdio MCP proxy wrapping the muxo API").action(async (opts) => {
|
|
1120
1467
|
process.exitCode = await mcp(opts);
|
|
1121
1468
|
});
|
|
1122
1469
|
await program.parseAsync(process.argv);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@muxoai/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Muxo CLI: one key for your whole stack — scaffold, validate, apply, run, and monitor agent workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"prepublishOnly": "npm run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@muxoai/core": "^0.1.
|
|
42
|
+
"@muxoai/core": "^0.1.1",
|
|
43
43
|
"commander": "^12.0.0",
|
|
44
44
|
"yaml": "^2.4.0",
|
|
45
45
|
"zod": "^3.23.0"
|