@aistoragedepot/mcp 0.4.0 → 0.5.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/README.md +22 -9
- package/index.mjs +1 -1
- package/package.json +1 -1
- package/pull.mjs +217 -63
package/README.md
CHANGED
|
@@ -52,30 +52,43 @@ Your stored content shows up where you actually work:
|
|
|
52
52
|
## Slash-commands (`pull`)
|
|
53
53
|
|
|
54
54
|
Not every client turns MCP prompts into `/` slash-commands (some only use the tools). To get
|
|
55
|
-
your library as **native** slash-commands
|
|
55
|
+
your library as **native** slash-commands, sync it to command files — for every AI tool you
|
|
56
|
+
use, not just Claude:
|
|
56
57
|
|
|
57
58
|
```bash
|
|
58
|
-
npx -y @aistoragedepot/mcp pull --token=aisd_your_token
|
|
59
|
+
npx -y @aistoragedepot/mcp pull --token=aisd_your_token --to=all
|
|
59
60
|
```
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
`
|
|
62
|
+
| Target | Writes to | Commands appear as |
|
|
63
|
+
| --- | --- | --- |
|
|
64
|
+
| `claude` *(default)* | `~/.claude/commands/aistoragedepot/` | `/aistoragedepot:<name>` |
|
|
65
|
+
| `cursor` | `~/.cursor/commands/` | `/aisd-<name>` |
|
|
66
|
+
| `vscode` | your VS Code profile's `prompts/` folder (Copilot Chat) | `/aisd-<name>` |
|
|
67
|
+
| `windsurf` | `~/.codeium/windsurf/global_workflows/` (Cascade) | `/aisd-<name>` |
|
|
68
|
+
| `codex` | `~/.codex/prompts/` | `/prompts:aisd-<name>` |
|
|
69
|
+
|
|
70
|
+
Tools that aren't installed are skipped automatically. Each tool gets its own dialect
|
|
71
|
+
(frontmatter, argument hints, `$ARGUMENTS` where supported). **Skills** run as-is.
|
|
72
|
+
**Prompts** take input: their `[fields]` show as an argument hint, and whatever you type
|
|
73
|
+
after the command fills them —
|
|
64
74
|
|
|
65
75
|
```
|
|
66
76
|
/aistoragedepot:cold-outreach Jane at Globex, about our payments API
|
|
67
77
|
```
|
|
68
78
|
|
|
69
|
-
— with
|
|
70
|
-
rewrites its own namespaced folder.
|
|
79
|
+
— with the AI asking for any value you left out.
|
|
71
80
|
|
|
72
81
|
| Option | Default | |
|
|
73
82
|
| --- | --- | --- |
|
|
83
|
+
| `--to=…` | `claude` | Which tools to write commands for — comma list, or `all`. |
|
|
74
84
|
| `--token=…` | `AISD_TOKEN` env | Your API token. |
|
|
75
85
|
| `--types=…` | `skill,prompt` | Which item types become commands (e.g. `skill` for skills only). |
|
|
76
86
|
| `--workspaces="A,B"` | your personal library | Which workspaces to pull (names, or `all`). |
|
|
77
|
-
| `--project` | off | Write
|
|
78
|
-
| `--dir=<path>` |
|
|
87
|
+
| `--project` | off | Write into the current repo instead: `.claude/commands/`, `.cursor/commands/`, `.github/prompts/`, `.windsurf/workflows/`. |
|
|
88
|
+
| `--dir=<path>` | per-target | Write somewhere else entirely (single `--to` only). |
|
|
89
|
+
|
|
90
|
+
Re-runs are safe: each target only clears its own files (claude: its namespaced folder;
|
|
91
|
+
the others: only files with the `aisd-` prefix — your own commands are never touched).
|
|
79
92
|
|
|
80
93
|
## Notes
|
|
81
94
|
|
package/index.mjs
CHANGED
|
@@ -141,7 +141,7 @@ function fillBody(body, placeholders, args) {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
const server = new Server(
|
|
144
|
-
{ name: "aistoragedepot", version: "0.
|
|
144
|
+
{ name: "aistoragedepot", version: "0.5.0" },
|
|
145
145
|
{ capabilities: { resources: {}, prompts: {}, tools: {} } },
|
|
146
146
|
);
|
|
147
147
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aistoragedepot/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Model Context Protocol server for your AIStorageDepot library — exposes your prompts, rules, docs, and skills to MCP-aware AI clients (Claude, Cursor, Cline, …).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/pull.mjs
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
// `aistoragedepot-mcp pull` — sync your library into native slash-command files
|
|
1
|
+
// `aistoragedepot-mcp pull` — sync your library into native slash-command files for the
|
|
2
|
+
// AI tools you use.
|
|
2
3
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// (
|
|
9
|
-
//
|
|
10
|
-
|
|
4
|
+
// Every major client now reads commands from a folder of markdown files; only the folder
|
|
5
|
+
// and the frontmatter dialect differ. Targets (--to=claude,cursor,vscode,windsurf,codex | all):
|
|
6
|
+
// claude ~/.claude/commands/aistoragedepot/*.md → /aistoragedepot:<name>
|
|
7
|
+
// cursor ~/.cursor/commands/aisd-*.md → /aisd-<name>
|
|
8
|
+
// vscode <VS Code User>/prompts/aisd-*.prompt.md → /aisd-<name> (Copilot Chat)
|
|
9
|
+
// windsurf ~/.codeium/windsurf/global_workflows/aisd-*.md → /aisd-<name> (Cascade)
|
|
10
|
+
// codex ~/.codex/prompts/aisd-*.md → /prompts:aisd-<name>
|
|
11
|
+
//
|
|
12
|
+
// Defaults to --to=claude with the **skills and prompts** in your personal library. Skills
|
|
13
|
+
// run as-is; prompts become commands that take input — their [fields] are declared as an
|
|
14
|
+
// argument-hint and filled from what you type after the command (real $ARGUMENTS where the
|
|
15
|
+
// tool substitutes it: claude + codex; a fill-from-my-message instruction elsewhere), with
|
|
16
|
+
// the AI asking for anything missing. Narrow with --types / widen with --workspaces.
|
|
17
|
+
// Re-runs clear only their own files (claude: its namespaced folder; others: the aisd- prefix).
|
|
18
|
+
import { mkdir, writeFile, rm, readdir } from "node:fs/promises";
|
|
19
|
+
import { existsSync } from "node:fs";
|
|
11
20
|
import { homedir } from "node:os";
|
|
12
21
|
import { join, resolve } from "node:path";
|
|
13
22
|
|
|
@@ -16,27 +25,150 @@ const slug = (s) =>
|
|
|
16
25
|
const typeOf = (it) => (it.type?.slug || it.type?.name || "").toLowerCase();
|
|
17
26
|
const isPromptItem = (it) => it.type?.format === "prompt" || typeOf(it) === "prompt";
|
|
18
27
|
|
|
19
|
-
// Prompts become commands that take input: the [fields] in the body are declared as an
|
|
20
|
-
// argument-hint (shown in the / picker) and filled from whatever the user types after the
|
|
21
|
-
// command ($ARGUMENTS); Claude asks for any value that's missing.
|
|
22
28
|
const FIELD_RE = /\[([A-Za-z0-9 ]+)\]/g;
|
|
23
29
|
const yq = (s) => String(s).replace(/"/g, "'").replace(/\s+/g, " ").trim(); // YAML-safe one-liner
|
|
30
|
+
const fieldsOf = (body) => [...new Set([...String(body).matchAll(FIELD_RE)].map((m) => m[1]))];
|
|
31
|
+
|
|
32
|
+
// Split a leading YAML frontmatter block off a body (skills carry their own). Values are
|
|
33
|
+
// only shallow-parsed — we just need `description` — everything else is passed through or
|
|
34
|
+
// dropped depending on the target.
|
|
35
|
+
function splitFrontmatter(body) {
|
|
36
|
+
const m = String(body).match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
37
|
+
if (!m) return { meta: {}, content: String(body) };
|
|
38
|
+
const meta = {};
|
|
39
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
40
|
+
const kv = line.match(/^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/);
|
|
41
|
+
if (kv) meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, "");
|
|
42
|
+
}
|
|
43
|
+
return { meta, content: String(body).slice(m[0].length) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function descFor(it, meta) {
|
|
47
|
+
if (meta.description) return meta.description;
|
|
48
|
+
return `${isPromptItem(it) ? "Prompt — " : ""}${it.title} (AIStorageDepot)`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function fmFile(fm, body) {
|
|
52
|
+
return ["---", ...Object.entries(fm).map(([k, v]) => `${k}: "${yq(v)}"`), "---", "", String(body).trim(), ""].join("\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Tail for tools that substitute $ARGUMENTS (claude, codex).
|
|
56
|
+
const argsTail = (fields) =>
|
|
57
|
+
fields.length
|
|
58
|
+
? `\n\nInput: $ARGUMENTS\n\nFill the bracketed ${fields.length === 1 ? "field" : "fields"} in the prompt above from the input — ask me for any value that's missing — then carry out the prompt.`
|
|
59
|
+
: "\n\n$ARGUMENTS";
|
|
60
|
+
// Tail for tools that just pass the typed text along with the inserted prompt.
|
|
61
|
+
const fillTail = (fields) =>
|
|
62
|
+
`\n\nFill the bracketed ${fields.length === 1 ? "field" : "fields"} (${fields.map((f) => `[${f}]`).join(", ")}) from anything I typed along with this command — ask me for any value that's missing — then carry out the prompt.`;
|
|
63
|
+
|
|
64
|
+
// ── per-target renderers ─────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
// Claude Code: skills keep their own frontmatter verbatim; prompts get description +
|
|
67
|
+
// argument-hint + $ARGUMENTS; anything else gets a description wrapper.
|
|
68
|
+
function renderClaude(it) {
|
|
69
|
+
const body = it.body || "";
|
|
70
|
+
if (isPromptItem(it)) {
|
|
71
|
+
const { content } = splitFrontmatter(body);
|
|
72
|
+
const fields = fieldsOf(content);
|
|
73
|
+
const fm = { description: `Prompt — ${it.title} (AIStorageDepot)` };
|
|
74
|
+
if (fields.length) fm["argument-hint"] = fields.map((f) => `[${f}]`).join(" ");
|
|
75
|
+
return fmFile(fm, content.trim() + argsTail(fields));
|
|
76
|
+
}
|
|
77
|
+
if (/^---\r?\n/.test(body)) return body.endsWith("\n") ? body : `${body}\n`;
|
|
78
|
+
return fmFile({ description: `${it.title} (AIStorageDepot)` }, body);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Codex custom prompts: same dialect as Claude (description, argument-hint, $ARGUMENTS),
|
|
82
|
+
// but Codex doesn't know claude-specific skill frontmatter — rebuild it clean.
|
|
83
|
+
function renderCodex(it) {
|
|
84
|
+
const { meta, content } = splitFrontmatter(it.body || "");
|
|
85
|
+
const fields = isPromptItem(it) ? fieldsOf(content) : [];
|
|
86
|
+
const fm = { description: descFor(it, meta) };
|
|
87
|
+
if (isPromptItem(it) && fields.length) fm["argument-hint"] = fields.map((f) => `[${f}]`).join(" ");
|
|
88
|
+
return fmFile(fm, content.trim() + (isPromptItem(it) ? argsTail(fields) : ""));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// VS Code prompt files: description + argument-hint frontmatter; typed extras arrive as
|
|
92
|
+
// part of the chat message (no $ARGUMENTS substitution).
|
|
93
|
+
function renderVscode(it) {
|
|
94
|
+
const { meta, content } = splitFrontmatter(it.body || "");
|
|
95
|
+
const fields = isPromptItem(it) ? fieldsOf(content) : [];
|
|
96
|
+
const fm = { description: descFor(it, meta) };
|
|
97
|
+
if (fields.length) fm["argument-hint"] = fields.map((f) => `[${f}]`).join(" ");
|
|
98
|
+
return fmFile(fm, content.trim() + (fields.length ? fillTail(fields) : ""));
|
|
99
|
+
}
|
|
24
100
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
101
|
+
// Windsurf workflows: description frontmatter; 12k char cap per file (enforced by caller).
|
|
102
|
+
function renderWindsurf(it) {
|
|
103
|
+
const { meta, content } = splitFrontmatter(it.body || "");
|
|
104
|
+
const fields = isPromptItem(it) ? fieldsOf(content) : [];
|
|
105
|
+
return fmFile({ description: descFor(it, meta) }, content.trim() + (fields.length ? fillTail(fields) : ""));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Cursor commands: plain markdown — no frontmatter dialect; the body IS the prompt.
|
|
109
|
+
function renderCursor(it) {
|
|
110
|
+
const { content } = splitFrontmatter(it.body || "");
|
|
111
|
+
const fields = isPromptItem(it) ? fieldsOf(content) : [];
|
|
112
|
+
return content.trim() + (fields.length ? fillTail(fields) : "") + "\n";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── targets ──────────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
function vscodeUserDir(home) {
|
|
118
|
+
if (process.platform === "win32")
|
|
119
|
+
return join(process.env.APPDATA || join(home, "AppData", "Roaming"), "Code", "User");
|
|
120
|
+
if (process.platform === "darwin") return join(home, "Library", "Application Support", "Code", "User");
|
|
121
|
+
return join(home, ".config", "Code", "User");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// prefix'd targets share folders with the user's own files — we only ever delete our prefix.
|
|
125
|
+
function targetDefs(project) {
|
|
126
|
+
const home = homedir();
|
|
127
|
+
return {
|
|
128
|
+
claude: {
|
|
129
|
+
dir: project ? resolve(".claude", "commands", "aistoragedepot") : join(home, ".claude", "commands", "aistoragedepot"),
|
|
130
|
+
parent: null, // Claude Code owns ~/.claude; safe to create
|
|
131
|
+
ownsFolder: true,
|
|
132
|
+
prefix: "",
|
|
133
|
+
ext: ".md",
|
|
134
|
+
render: renderClaude,
|
|
135
|
+
tip: "type /aistoragedepot in a new chat (first sync: fully restart Claude Code — end claude.exe on Windows)",
|
|
136
|
+
},
|
|
137
|
+
cursor: {
|
|
138
|
+
dir: project ? resolve(".cursor", "commands") : join(home, ".cursor", "commands"),
|
|
139
|
+
parent: project ? null : join(home, ".cursor"),
|
|
140
|
+
prefix: "aisd-",
|
|
141
|
+
ext: ".md",
|
|
142
|
+
render: renderCursor,
|
|
143
|
+
tip: "type /aisd- in Cursor's Agent input",
|
|
144
|
+
},
|
|
145
|
+
vscode: {
|
|
146
|
+
dir: project ? resolve(".github", "prompts") : join(vscodeUserDir(home), "prompts"),
|
|
147
|
+
parent: project ? null : vscodeUserDir(home),
|
|
148
|
+
prefix: "aisd-",
|
|
149
|
+
ext: ".prompt.md",
|
|
150
|
+
render: renderVscode,
|
|
151
|
+
tip: "type /aisd- in Copilot Chat",
|
|
152
|
+
},
|
|
153
|
+
windsurf: {
|
|
154
|
+
dir: project ? resolve(".windsurf", "workflows") : join(home, ".codeium", "windsurf", "global_workflows"),
|
|
155
|
+
parent: project ? null : join(home, ".codeium", "windsurf"),
|
|
156
|
+
prefix: "aisd-",
|
|
157
|
+
ext: ".md",
|
|
158
|
+
render: renderWindsurf,
|
|
159
|
+
maxBytes: 12000, // Windsurf rejects workflow files past ~12k chars
|
|
160
|
+
tip: "type /aisd- in Cascade",
|
|
161
|
+
},
|
|
162
|
+
codex: {
|
|
163
|
+
dir: join(process.env.CODEX_HOME || join(home, ".codex"), "prompts"), // global only
|
|
164
|
+
parent: process.env.CODEX_HOME || join(home, ".codex"),
|
|
165
|
+
prefix: "aisd-",
|
|
166
|
+
ext: ".md",
|
|
167
|
+
render: renderCodex,
|
|
168
|
+
noProject: true,
|
|
169
|
+
tip: "type /prompts:aisd- (or /aisd-) in Codex — restart codex to load",
|
|
170
|
+
},
|
|
171
|
+
};
|
|
40
172
|
}
|
|
41
173
|
|
|
42
174
|
function parseArgs(argv) {
|
|
@@ -68,6 +200,20 @@ export async function pull({ BASE_URL, argv }) {
|
|
|
68
200
|
return r.json();
|
|
69
201
|
};
|
|
70
202
|
|
|
203
|
+
// Which tools to write for. Default: claude. --to=cursor,vscode | all.
|
|
204
|
+
const DEFS = targetDefs(!!o.project);
|
|
205
|
+
const toRaw = String(o.to || "claude").toLowerCase().split(",").map((s) => s.trim()).filter(Boolean);
|
|
206
|
+
const targets = toRaw.includes("all") ? Object.keys(DEFS) : toRaw;
|
|
207
|
+
const unknown = targets.filter((t) => !DEFS[t]);
|
|
208
|
+
if (unknown.length) {
|
|
209
|
+
console.error(`Unknown --to target(s): ${unknown.join(", ")}. Valid: ${Object.keys(DEFS).join(", ")}, all.`);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
if (o.dir && targets.length > 1) {
|
|
213
|
+
console.error("--dir only makes sense with a single --to target.");
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
71
217
|
// Which item types to pull. Default: skills + prompts. --types=skill | prompt | all.
|
|
72
218
|
const TYPES = String(o.types || "skill,prompt").toLowerCase().split(",").map((s) => s.trim()).filter(Boolean);
|
|
73
219
|
const wantType = (it) => {
|
|
@@ -79,52 +225,60 @@ export async function pull({ BASE_URL, argv }) {
|
|
|
79
225
|
};
|
|
80
226
|
|
|
81
227
|
const workspaces = await api("/api/workspaces");
|
|
82
|
-
// Scope: default
|
|
228
|
+
// Scope: default your personal library; --workspaces="A,B" | "all".
|
|
83
229
|
const names = String(o.workspaces || "").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
84
230
|
let scope;
|
|
85
231
|
if (names.includes("all") || names.includes("*")) scope = workspaces;
|
|
86
232
|
else if (names.length) scope = workspaces.filter((w) => names.includes((w.name || "").toLowerCase()));
|
|
87
|
-
else scope = workspaces.filter((w) => w.type === "PERSONAL");
|
|
233
|
+
else scope = workspaces.filter((w) => w.type === "PERSONAL");
|
|
88
234
|
|
|
89
235
|
const libs = await Promise.all(
|
|
90
236
|
scope.map((w) => api(`/api/library?workspace=${encodeURIComponent(w.id)}`).catch(() => ({ items: [] }))),
|
|
91
237
|
);
|
|
92
238
|
const items = libs.flatMap((l) => (l.items || []).filter(wantType));
|
|
93
239
|
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
240
|
+
for (const tname of targets) {
|
|
241
|
+
const t = DEFS[tname];
|
|
242
|
+
if (t.noProject && o.project) console.log(` (${tname} has no per-project location — writing globally)`);
|
|
243
|
+
// Don't invent config folders for tools that aren't installed (claude is exempt: it's
|
|
244
|
+
// our home target and Claude Code tolerates a pre-made commands dir).
|
|
245
|
+
if (t.parent && !existsSync(t.parent)) {
|
|
246
|
+
console.log(`↷ ${tname} skipped — ${t.parent} not found (is it installed? use --to without it, or install first)`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const dir = o.dir && targets.length === 1 ? resolve(String(o.dir)) : t.dir;
|
|
250
|
+
if (t.ownsFolder) await rm(dir, { recursive: true, force: true }); // clean re-sync of OUR folder only
|
|
251
|
+
await mkdir(dir, { recursive: true });
|
|
252
|
+
if (!t.ownsFolder) {
|
|
253
|
+
// shared folder: remove only files we wrote before (our prefix)
|
|
254
|
+
const existing = await readdir(dir).catch(() => []);
|
|
255
|
+
await Promise.all(
|
|
256
|
+
existing.filter((n) => n.startsWith(t.prefix) && n.endsWith(t.ext)).map((n) => rm(join(dir, n), { force: true })),
|
|
257
|
+
);
|
|
111
258
|
}
|
|
112
|
-
while (used.has(name)) name = `${name}-${it.id.slice(-4)}`;
|
|
113
|
-
used.add(name);
|
|
114
|
-
const body = it.body || "";
|
|
115
|
-
// Prompts get the argument treatment. Skills already carry their own YAML frontmatter
|
|
116
|
-
// (name/description) — keep them as-is; anything else without frontmatter gets a
|
|
117
|
-
// description so it shows nicely in the / menu.
|
|
118
|
-
const content = isPromptItem(it)
|
|
119
|
-
? promptCommand(it)
|
|
120
|
-
: /^---\r?\n/.test(body)
|
|
121
|
-
? body.endsWith("\n") ? body : `${body}\n`
|
|
122
|
-
: `---\ndescription: "${yq(`${it.title} (AIStorageDepot)`)}"\n---\n\n${body}\n`;
|
|
123
|
-
await writeFile(join(dir, `${name}.md`), content, "utf8");
|
|
124
|
-
n++;
|
|
125
|
-
}
|
|
126
259
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
260
|
+
const used = new Set();
|
|
261
|
+
let n = 0;
|
|
262
|
+
let skipped = 0;
|
|
263
|
+
for (const it of items) {
|
|
264
|
+
// Clean names; on a title collision, the type breaks the tie before an id suffix.
|
|
265
|
+
let name = slug(it.title);
|
|
266
|
+
if (used.has(name)) {
|
|
267
|
+
const alt = slug(`${it.title} ${typeOf(it) || "item"}`);
|
|
268
|
+
name = used.has(alt) ? `${slug(it.title)}-${it.id.slice(-4)}` : alt;
|
|
269
|
+
}
|
|
270
|
+
while (used.has(name)) name = `${name}-${it.id.slice(-4)}`;
|
|
271
|
+
used.add(name);
|
|
272
|
+
const content = t.render(it);
|
|
273
|
+
if (t.maxBytes && content.length > t.maxBytes) {
|
|
274
|
+
console.log(` ! ${tname}: skipped "${it.title}" — over the ${t.maxBytes}-char limit`);
|
|
275
|
+
skipped++;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
await writeFile(join(dir, `${t.prefix}${name}${t.ext}`), content, "utf8");
|
|
279
|
+
n++;
|
|
280
|
+
}
|
|
281
|
+
console.log(`✓ ${tname} — ${n} command${n === 1 ? "" : "s"}${skipped ? ` (${skipped} skipped)` : ""} → ${dir}`);
|
|
282
|
+
console.log(` ${t.tip}`);
|
|
283
|
+
}
|
|
130
284
|
}
|