create-deck-plugin 0.15.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/index.mjs +77 -0
- package/package.json +20 -0
- package/template/README.md +17 -0
- package/template/gitignore +4 -0
- package/template/package.json +10 -0
- package/template/plugin.json +8 -0
- package/template/src/plugin.tsx +65 -0
- package/template/tsconfig.json +13 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Scaffold a Deck plugin.
|
|
3
|
+
//
|
|
4
|
+
// npm create deck-plugin my-thing
|
|
5
|
+
//
|
|
6
|
+
// Writes a folder that already builds, already has a git repository, and already knows how to
|
|
7
|
+
// install itself into Deck. The real work is in deck-plugin-sdk, which this installs — a
|
|
8
|
+
// scaffold on its own would freeze the shim and the types at the moment of creation, which is
|
|
9
|
+
// the thing that rots.
|
|
10
|
+
import { execFileSync } from "node:child_process";
|
|
11
|
+
import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
const raw = process.argv[2];
|
|
18
|
+
if (!raw) {
|
|
19
|
+
console.error("usage: npm create deck-plugin <name>");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The id is a config key and a path component in Deck, so it is checked here rather than at
|
|
24
|
+
// install time. Same rule the loader applies.
|
|
25
|
+
const id = raw.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
26
|
+
if (!id || !/^[a-z0-9]/.test(id)) {
|
|
27
|
+
console.error(`"${raw}" does not reduce to a usable id (letters, digits and dashes)`);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const dir = resolve(process.cwd(), id);
|
|
32
|
+
if (existsSync(dir) && readdirSync(dir).length) {
|
|
33
|
+
console.error(`${dir} already exists and is not empty`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const name = id.split("-").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
38
|
+
const component = name.replace(/[^A-Za-z0-9]/g, "") || "Plugin";
|
|
39
|
+
|
|
40
|
+
// The template is the SDK's — one copy, so the scaffold and the thing it installs cannot
|
|
41
|
+
// disagree. Resolved through node rather than by relative path, because once published these
|
|
42
|
+
// are two packages rather than two folders.
|
|
43
|
+
const sdkTemplate = existsSync(join(here, "template"))
|
|
44
|
+
? join(here, "template") // published: copied in at pack time
|
|
45
|
+
: join(here, "..", "sdk", "template"); // in the repo: the SDK's own
|
|
46
|
+
cpSync(sdkTemplate, dir, { recursive: true });
|
|
47
|
+
// npm refuses to publish a file called .gitignore, so the template ships it renamed.
|
|
48
|
+
if (existsSync(join(dir, "gitignore"))) renameSync(join(dir, "gitignore"), join(dir, ".gitignore"));
|
|
49
|
+
|
|
50
|
+
for (const rel of ["plugin.json", "package.json", "src/plugin.tsx", "README.md"]) {
|
|
51
|
+
const p = join(dir, rel);
|
|
52
|
+
if (!existsSync(p)) continue;
|
|
53
|
+
writeFileSync(p, readFileSync(p, "utf8")
|
|
54
|
+
.replaceAll("__ID__", id)
|
|
55
|
+
.replaceAll("__NAME__", name)
|
|
56
|
+
.replaceAll("__COMPONENT__", component));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const run = (cmd, args) => {
|
|
60
|
+
try { execFileSync(cmd, args, { cwd: dir, stdio: "inherit" }); return true; }
|
|
61
|
+
catch { return false; }
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
console.log(`\nInstalling deck-plugin-sdk…`);
|
|
65
|
+
const npm = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
66
|
+
const installed = run(npm, ["install", "--save-dev", "deck-plugin-sdk",
|
|
67
|
+
"react", "lucide-react", "@types/react"]);
|
|
68
|
+
|
|
69
|
+
// A git repository from the start, because a plugin's home is its own repository — that is how
|
|
70
|
+
// it gets published, and how it gets listed in the registry.
|
|
71
|
+
run("git", ["init", "-q", "-b", "main"]);
|
|
72
|
+
|
|
73
|
+
console.log(`\n${name} is in ${id}/\n`);
|
|
74
|
+
console.log(" cd " + id);
|
|
75
|
+
if (!installed) console.log(" npm install # this failed; run it yourself");
|
|
76
|
+
console.log(" npm run dev # build, and rebuild as you edit");
|
|
77
|
+
console.log(" npm run install-local # copy it into Deck and load it\n");
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-deck-plugin",
|
|
3
|
+
"version": "0.15.1",
|
|
4
|
+
"description": "Scaffold a Deck plugin.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"create-deck-plugin": "index.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.mjs",
|
|
12
|
+
"template"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"prepack": "node prepack.mjs"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# __NAME__
|
|
2
|
+
|
|
3
|
+
A plugin for [Deck](https://deck.hasan-zarif.com).
|
|
4
|
+
|
|
5
|
+
## Developing
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm run dev # build, and rebuild as you edit
|
|
9
|
+
npm run install-local # copy it into Deck, which loads it without a restart
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
`src/plugin.tsx` is the entry point; its default export is the tab.
|
|
13
|
+
|
|
14
|
+
## What it does
|
|
15
|
+
|
|
16
|
+
Say what this plugin is for. This file is what the registry shows on its page, so it is worth
|
|
17
|
+
writing for somebody deciding whether to install it rather than for yourself.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// __NAME__ — a Deck plugin.
|
|
2
|
+
//
|
|
3
|
+
// The default export is the tab. A plugin can export more than that — a footer readout, a
|
|
4
|
+
// settings panel, palette commands, a pane view — and `plugins/API.md` in the Deck repository
|
|
5
|
+
// lists every slot. One export is enough to start.
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
import { Sparkles } from "lucide-react";
|
|
8
|
+
import { configRead, configWrite } from "../shim/bridge.js";
|
|
9
|
+
|
|
10
|
+
/** Where this plugin's own settings live. The `plugin-` prefix keeps them on this machine. */
|
|
11
|
+
const CONFIG = "plugin-__ID__";
|
|
12
|
+
|
|
13
|
+
export default function __COMPONENT__() {
|
|
14
|
+
const [note, setNote] = useState("");
|
|
15
|
+
const [saved, setSaved] = useState(false);
|
|
16
|
+
|
|
17
|
+
// Deck's config store, the same one its own modules use. Reading a key that has never been
|
|
18
|
+
// written resolves to an empty string rather than throwing.
|
|
19
|
+
const load = async () => {
|
|
20
|
+
const raw = await configRead(CONFIG).catch(() => "");
|
|
21
|
+
setNote(raw.trim() ? (JSON.parse(raw) as { note?: string }).note ?? "" : "");
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const save = async () => {
|
|
25
|
+
await configWrite(CONFIG, JSON.stringify({ note }));
|
|
26
|
+
setSaved(true);
|
|
27
|
+
setTimeout(() => setSaved(false), 1500);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex h-full flex-col gap-4 p-6">
|
|
32
|
+
<header className="flex items-center gap-3">
|
|
33
|
+
<Sparkles size={18} style={{ color: "var(--accent)" }} />
|
|
34
|
+
<h1 className="text-base font-semibold text-text-primary">__NAME__</h1>
|
|
35
|
+
</header>
|
|
36
|
+
|
|
37
|
+
<p className="max-w-[60ch] text-sm text-text-secondary">
|
|
38
|
+
Edit <code className="rounded bg-elev px-1.5 py-0.5 text-text-primary">src/plugin.tsx</code>,
|
|
39
|
+
run <code className="rounded bg-elev px-1.5 py-0.5 text-text-primary">npm run dev</code>,
|
|
40
|
+
and Deck reloads the plugin as you save.
|
|
41
|
+
</p>
|
|
42
|
+
|
|
43
|
+
<div className="flex max-w-md items-center gap-2">
|
|
44
|
+
<input
|
|
45
|
+
value={note}
|
|
46
|
+
onChange={(e) => setNote(e.target.value)}
|
|
47
|
+
onFocus={() => void load()}
|
|
48
|
+
placeholder="Type something, then save it"
|
|
49
|
+
className="min-w-0 flex-1 rounded-lg border border-subtle bg-elev px-3 py-2 text-sm text-text-primary outline-none focus:border-accent"
|
|
50
|
+
/>
|
|
51
|
+
<button
|
|
52
|
+
onClick={() => void save()}
|
|
53
|
+
className="shrink-0 rounded-lg border border-subtle bg-elev px-3 py-2 text-sm text-text-secondary hover:border-accent hover:text-text-primary"
|
|
54
|
+
>
|
|
55
|
+
{saved ? "Saved" : "Save"}
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<p className="text-xs text-text-muted">
|
|
60
|
+
That writes to Deck's config store under <code>{CONFIG}</code>, which stays on this
|
|
61
|
+
machine.
|
|
62
|
+
</p>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|