Skip to content

Commit 0a8a90f

Browse files
Add JSON output for template list (#284)
1 parent 5fde89b commit 0a8a90f

8 files changed

Lines changed: 54 additions & 3 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ something to put behind it:
249249

250250
```sh
251251
moshcode template list # what there is
252+
moshcode template list --json # machine-readable template metadata
252253
moshcode template install bun-caddy-sqlite # into the current directory
253254
moshcode template install caddy-static --into /srv/site
254255
moshcode template install owner/repo # or a git URL, or a .tar.gz

src/cli-schema.mjs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,15 @@ export const CORE_CLI_COMMANDS = [
229229
group: "hosting",
230230
description: "scaffold a stack for a Moshpit-hosted service",
231231
synopsis: [
232-
["moshcode template list", "what there is"],
232+
["moshcode template list [--json]", "what there is"],
233233
["moshcode template install <name>", "write it here"],
234234
],
235+
flags: [
236+
["--into <dir>", "write somewhere other than the current directory", ""],
237+
["--force", "overwrite files that are already there", ""],
238+
["--dry-run", "show changes without writing anything", ""],
239+
["--json", "machine-readable template list", ""],
240+
],
235241
examples: [["moshcode template install bun-caddy-sqlite", ""]],
236242
seeAlso: ["site"],
237243
},

src/completion.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ Register-ArgumentCompleter -Native -CommandName moshcode -ScriptBlock {
205205
{ $_ -in @('template', 'templates') } {
206206
if ($argumentIndex -eq 2) {
207207
$choices = $script:MoshcodeCompletionTemplate
208+
} elseif ($nested -eq 'list' -and $wordToComplete.StartsWith('-')) {
209+
$choices = $script:MoshcodeCompletionJson
208210
} elseif ($nested -eq 'install' -and $wordToComplete.StartsWith('-')) {
209211
$choices = $script:MoshcodeCompletionTemplateInstall
210212
}
@@ -306,6 +308,8 @@ _moshcode_completion() {
306308
template|templates)
307309
if (( COMP_CWORD == 2 )); then
308310
choices="list install"
311+
elif [[ "$nested" == "list" && "$cur" == -* ]]; then
312+
choices="--json"
309313
elif [[ "$nested" == "install" && "$cur" == -* ]]; then
310314
choices="--into --force --dry-run"
311315
fi
@@ -432,6 +436,8 @@ _moshcode() {
432436
template|templates)
433437
if (( CURRENT == 3 )); then
434438
_values "template command" list install
439+
elif [[ "\${words[3]}" == "list" && "$PREFIX" == -* ]]; then
440+
_values "template list option" --json
435441
elif [[ "\${words[3]}" == "install" && "$PREFIX" == -* ]]; then
436442
_values "template install option" --into --force --dry-run
437443
else
@@ -504,6 +510,7 @@ complete -c moshcode -n '__moshcode_nested_is dns resolve' -l json -d 'print JSO
504510
complete -c moshcode -n '__moshcode_nested_is dns resolve' -l open -d 'open a parked name in the Pit'
505511
complete -c moshcode -n '__moshcode_nested_is dns resolve' -l registry -r -d 'registry base URL'
506512
complete -c moshcode -n '${atSecondToken("template templates")}' -a 'list install' -d 'template command'
513+
complete -c moshcode -n '__moshcode_nested_is template list; or __moshcode_nested_is templates list' -l json -d 'print JSON'
507514
complete -c moshcode -n '__moshcode_nested_is template install; or __moshcode_nested_is templates install' -l into -r -d 'target directory'
508515
complete -c moshcode -n '__moshcode_nested_is template install; or __moshcode_nested_is templates install' -l force -d 'overwrite existing files'
509516
complete -c moshcode -n '__moshcode_nested_is template install; or __moshcode_nested_is templates install' -l dry-run -d 'preview without writing'

src/templates.mjs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,14 +275,15 @@ export async function fetchRemote(
275275

276276
const USAGE = `moshcode template — starting stacks for Moshpit-hosted services
277277
278-
moshcode template list the templates that ship with moshcode
278+
moshcode template list [--json] the templates that ship with moshcode
279279
moshcode template install <name> copy a bundled one into this directory
280280
moshcode template install <url> copy one from a git repo or a .tar.gz
281281
moshcode template install <owner/repo> the GitHub shorthand
282282
283283
--into <dir> write somewhere other than the current directory
284284
--force overwrite files that are already there
285285
--dry-run show every create/overwrite without writing anything
286+
--json print the template list as JSON
286287
287288
Nothing in a template is executed on install — the files are copied and what to
288289
run is yours to decide. Read them before you do.`;
@@ -339,7 +340,16 @@ export async function templateCommand(
339340
}
340341

341342
if (sub === "list") {
343+
const unknown = rest.filter((arg) => arg !== "--json");
344+
if (unknown.length) {
345+
out(`moshcode template list: unknown option ${JSON.stringify(unknown[0])}`);
346+
return 1;
347+
}
342348
const templates = await listTemplates();
349+
if (rest.includes("--json")) {
350+
out(JSON.stringify(templates, null, 2));
351+
return 0;
352+
}
343353
if (!templates.length) {
344354
out("no templates bundled with this install");
345355
return 0;

test/completion-powershell.test.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,14 @@ test("PowerShell completes commands, install targets, and nested options", (t) =
5959
const installs = expand(executable, "moshcode install cl");
6060
const options = expand(executable, "moshcode mcp list --j");
6161
const templateCommands = expand(executable, "moshcode template ins");
62+
const templateListOptions = expand(executable, "moshcode template list --j");
6263
const templateOptions = expand(executable, "moshcode template install --d");
6364
const templateAliasOptions = expand(executable, "moshcode templates install --i");
6465
assert.ok(commands.includes("engines"), JSON.stringify(commands));
6566
assert.ok(installs.includes("claude"), JSON.stringify(installs));
6667
assert.ok(options.includes("--json"), JSON.stringify(options));
6768
assert.ok(templateCommands.includes("install"), JSON.stringify(templateCommands));
69+
assert.ok(templateListOptions.includes("--json"), JSON.stringify(templateListOptions));
6870
assert.ok(templateOptions.includes("--dry-run"), JSON.stringify(templateOptions));
6971
assert.ok(templateAliasOptions.includes("--into"), JSON.stringify(templateAliasOptions));
7072
});

test/completion.test.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ test("bash completion respects argument depth and preserves file fallbacks", ()
132132
assert.ok(bashCompletions(["moshcode", "dns", ""]).includes("resolve"));
133133
assert.ok(bashCompletions(["moshcode", "dns", "resolve", "--j"]).includes("--json"));
134134
assert.ok(bashCompletions(["moshcode", "template", ""]).includes("install"));
135+
assert.ok(bashCompletions(["moshcode", "template", "list", "--j"]).includes("--json"));
136+
assert.ok(bashCompletions(["moshcode", "templates", "list", "--j"]).includes("--json"));
135137
assert.ok(bashCompletions(["moshcode", "template", "install", "--"]).includes("--dry-run"));
136138

137139
assert.deepEqual(bashCompletions(["moshcode", "run", ""]), []);

test/help.test.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ test("flags that the dispatcher parses are documented", () => {
246246
for (const flag of verb.flags) for (const f of flag.flags.split(/[ ,]+/)) documented.add(f.replace(/[<>].*$/, "").trim());
247247
}
248248
}
249-
for (const flag of ["--json", "--max", "-n", "--dry-run", "--yes", "-y", "--device", "-d",
249+
for (const flag of ["--json", "--max", "-n", "--dry-run", "--into", "--force", "--yes", "-y", "--device", "-d",
250250
"--browser", "-b", "--port", "--bind", "--ttyd", "--url", "--name", "-t", "-e", "-H",
251251
"--registry", "--check", "--nginx", "--all"]) {
252252
assert.ok(documented.has(flag), `${flag} is parsed somewhere but documented nowhere`);

test/templates.test.mjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,29 @@ test("the bundled templates are listed with what they are for", async () => {
3939
}
4040
});
4141

42+
test("template list --json prints the stable template metadata", async () => {
43+
const lines = [];
44+
const code = await templateCommand(["list", "--json"], (line) => lines.push(line));
45+
const templates = JSON.parse(lines.join("\n"));
46+
47+
assert.equal(code, 0);
48+
assert.ok(Array.isArray(templates));
49+
assert.deepEqual(templates.map((template) => template.name), ["bun-caddy-sqlite", "caddy-static"]);
50+
for (const template of templates) {
51+
assert.deepEqual(Object.keys(template), ["name", "description"]);
52+
assert.equal(typeof template.description, "string");
53+
assert.ok(template.description.length > 0, `${template.name} has no description`);
54+
}
55+
});
56+
57+
test("template list rejects options it does not understand", async () => {
58+
const lines = [];
59+
const code = await templateCommand(["list", "--jsn"], (line) => lines.push(line));
60+
61+
assert.equal(code, 1);
62+
assert.deepEqual(lines, ['moshcode template list: unknown option "--jsn"']);
63+
});
64+
4265
test("an archive member that escapes the target is refused", () => {
4366
// Each of these writes outside the directory the user chose.
4467
for (const evil of [

0 commit comments

Comments
 (0)