@notur/sdk 1.4.5 → 1.4.6
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 +92 -1
- package/bin/notur-create.js +580 -0
- package/bin/notur-push.js +331 -0
- package/bin/notur.js +40 -0
- package/examples/red-button/.env.example +2 -0
- package/examples/red-button/README.md +30 -0
- package/examples/red-button/extension.yaml +19 -0
- package/examples/red-button/package.json +26 -0
- package/examples/red-button/resources/frontend/src/index.tsx +32 -0
- package/examples/red-button/src/RedButtonExtension.php +11 -0
- package/examples/red-button/tsconfig.json +13 -0
- package/examples/red-button/webpack.config.js +17 -0
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -94,7 +94,56 @@ import type {
|
|
|
94
94
|
|
|
95
95
|
## CLI Tools
|
|
96
96
|
|
|
97
|
-
The SDK ships
|
|
97
|
+
The SDK ships local development CLI tools, available via `npx`, `yarn dlx`, `pnpm dlx`, or `bunx`.
|
|
98
|
+
|
|
99
|
+
You can use the dispatcher:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npx @notur/sdk create acme/red-button
|
|
103
|
+
npx @notur/sdk pack
|
|
104
|
+
npx @notur/sdk push --host https://panel.example.com --key notur_xxx
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Or the standalone bins:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npx notur-create acme/red-button
|
|
111
|
+
npx notur-pack
|
|
112
|
+
npx notur-push --host https://panel.example.com --key notur_xxx
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `notur-create` — Scaffold extensions locally
|
|
116
|
+
|
|
117
|
+
Creates a Notur extension folder without requiring Pterodactyl, Laravel, or `php artisan`.
|
|
118
|
+
|
|
119
|
+
Run without arguments to start the interactive wizard:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npx notur-create
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Scripted usage stays non-interactive:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx notur-create acme/red-button --slot server.header
|
|
129
|
+
cd red-button
|
|
130
|
+
npm install
|
|
131
|
+
npm run build
|
|
132
|
+
npx notur-pack
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Options:
|
|
136
|
+
- `--path <dir>` — parent directory for the generated extension.
|
|
137
|
+
- `--preset <name>` — `frontend`, `backend`, `full`, or `minimal`.
|
|
138
|
+
- `--name <name>` — display name for `extension.yaml`.
|
|
139
|
+
- `--description <text>` — description for `extension.yaml`.
|
|
140
|
+
- `--slot <slot>` — initial frontend slot, default `dashboard.widgets`.
|
|
141
|
+
- `--package-manager <name>` — `npm`, `pnpm`, `yarn`, or `bun`.
|
|
142
|
+
- `--install` / `--no-install` — install frontend dependencies after scaffolding.
|
|
143
|
+
- `--env` / `--no-env` — create `.env` from `.env.example`.
|
|
144
|
+
- `--no-frontend` — generate only `extension.yaml` and the PHP entrypoint.
|
|
145
|
+
- `--with-api-routes` — include a client API route stub.
|
|
146
|
+
- `--force` — allow writing into an existing empty directory.
|
|
98
147
|
|
|
99
148
|
### `notur-pack` — Package extensions
|
|
100
149
|
|
|
@@ -137,6 +186,48 @@ This additionally produces:
|
|
|
137
186
|
|
|
138
187
|
The `.sig` format is compatible with PHP's `SignatureVerifier::verify()`.
|
|
139
188
|
|
|
189
|
+
### `notur-push` — Push to a remote Notur panel
|
|
190
|
+
|
|
191
|
+
Packages the local extension and uploads it to a Notur-enabled Pterodactyl panel using a remote push key.
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npx notur-push --host https://panel.example.com --key notur_xxx
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Equivalent values can be provided by the shell or by a local `.env` file in the extension directory:
|
|
198
|
+
|
|
199
|
+
```dotenv
|
|
200
|
+
NOTUR_HOST=https://panel.example.com
|
|
201
|
+
NOTUR_PUSH_KEY=notur_xxx
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Then run:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npm run push
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
or:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
npx notur-push
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Remote setup on the panel:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
php artisan notur:remote-key
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Add the printed `NOTUR_REMOTE_PUSH_ENABLED=true` and `NOTUR_REMOTE_PUSH_KEYS=...` values to the panel `.env`.
|
|
223
|
+
|
|
224
|
+
Options:
|
|
225
|
+
- `--archive <file>` — upload an existing `.notur` archive instead of packing.
|
|
226
|
+
- `--env-file <file>` — load host/key values from a custom env file.
|
|
227
|
+
- `--no-build` — skip running the local `build` script before packing.
|
|
228
|
+
- `--no-force` — do not overwrite an already installed extension.
|
|
229
|
+
- `--keep-archive` — keep the temporary archive generated for the push.
|
|
230
|
+
|
|
140
231
|
### `notur-keygen` — Generate signing keypair
|
|
141
232
|
|
|
142
233
|
Generates a new Ed25519 keypair for extension signing.
|
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const readline = require('readline');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
function parseArgs() {
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const options = {
|
|
11
|
+
id: null,
|
|
12
|
+
path: process.cwd(),
|
|
13
|
+
preset: null,
|
|
14
|
+
displayName: null,
|
|
15
|
+
description: null,
|
|
16
|
+
withFrontend: true,
|
|
17
|
+
withApiRoutes: false,
|
|
18
|
+
force: false,
|
|
19
|
+
slot: 'dashboard.widgets',
|
|
20
|
+
packageManager: null,
|
|
21
|
+
install: null,
|
|
22
|
+
createEnv: null,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
const arg = args[i];
|
|
27
|
+
if (arg === '--path') {
|
|
28
|
+
options.path = args[++i];
|
|
29
|
+
} else if (arg === '--preset') {
|
|
30
|
+
options.preset = args[++i];
|
|
31
|
+
} else if (arg === '--name' || arg === '--display-name') {
|
|
32
|
+
options.displayName = args[++i];
|
|
33
|
+
} else if (arg === '--description') {
|
|
34
|
+
options.description = args[++i];
|
|
35
|
+
} else if (arg === '--with-frontend') {
|
|
36
|
+
options.withFrontend = true;
|
|
37
|
+
} else if (arg === '--no-frontend') {
|
|
38
|
+
options.withFrontend = false;
|
|
39
|
+
} else if (arg === '--with-api-routes') {
|
|
40
|
+
options.withApiRoutes = true;
|
|
41
|
+
} else if (arg === '--no-api-routes') {
|
|
42
|
+
options.withApiRoutes = false;
|
|
43
|
+
} else if (arg === '--force') {
|
|
44
|
+
options.force = true;
|
|
45
|
+
} else if (arg === '--slot') {
|
|
46
|
+
options.slot = args[++i];
|
|
47
|
+
} else if (arg === '--package-manager') {
|
|
48
|
+
options.packageManager = args[++i];
|
|
49
|
+
} else if (arg === '--install') {
|
|
50
|
+
options.install = true;
|
|
51
|
+
} else if (arg === '--no-install') {
|
|
52
|
+
options.install = false;
|
|
53
|
+
} else if (arg === '--env') {
|
|
54
|
+
options.createEnv = true;
|
|
55
|
+
} else if (arg === '--no-env') {
|
|
56
|
+
options.createEnv = false;
|
|
57
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
58
|
+
usage(0);
|
|
59
|
+
} else if (!arg.startsWith('-') && !options.id) {
|
|
60
|
+
options.id = arg;
|
|
61
|
+
} else {
|
|
62
|
+
console.error(`Unknown argument: ${arg}`);
|
|
63
|
+
usage(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (options.preset && !['frontend', 'backend', 'full', 'minimal'].includes(options.preset)) {
|
|
68
|
+
console.error('Error: preset must be one of frontend, backend, full, or minimal.');
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (options.packageManager && !['npm', 'pnpm', 'yarn', 'bun'].includes(options.packageManager)) {
|
|
73
|
+
console.error('Error: package manager must be one of npm, pnpm, yarn, or bun.');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return options;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function usage(code) {
|
|
81
|
+
console.log(`Usage:
|
|
82
|
+
npx notur-create acme/red-button [options]
|
|
83
|
+
npx @notur/sdk create acme/red-button [options]
|
|
84
|
+
|
|
85
|
+
Options:
|
|
86
|
+
--path <dir> Parent directory for the generated extension
|
|
87
|
+
--preset <name> frontend, backend, full, or minimal
|
|
88
|
+
--name <name> Display name for extension.yaml
|
|
89
|
+
--description <txt> Description for extension.yaml
|
|
90
|
+
--slot <slot> Initial frontend slot (default: dashboard.widgets)
|
|
91
|
+
--package-manager npm, pnpm, yarn, or bun
|
|
92
|
+
--install Install frontend dependencies after scaffolding
|
|
93
|
+
--env Create .env from .env.example
|
|
94
|
+
--no-frontend Generate PHP/manifest only
|
|
95
|
+
--with-api-routes Include a client API route stub
|
|
96
|
+
--force Allow writing into an existing empty directory`);
|
|
97
|
+
process.exit(code);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isInteractive() {
|
|
101
|
+
return process.stdin.isTTY && process.stdout.isTTY;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function prompt(question, defaultValue = '') {
|
|
105
|
+
const suffix = defaultValue ? ` (${defaultValue})` : '';
|
|
106
|
+
const rl = readline.createInterface({
|
|
107
|
+
input: process.stdin,
|
|
108
|
+
output: process.stdout,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return new Promise(resolve => {
|
|
112
|
+
rl.question(`${question}${suffix}: `, answer => {
|
|
113
|
+
rl.close();
|
|
114
|
+
resolve(answer.trim() || defaultValue);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function select(question, choices, defaultChoice) {
|
|
120
|
+
const labels = choices.map(choice => choice.value).join('/');
|
|
121
|
+
while (true) {
|
|
122
|
+
const answer = await prompt(`${question} [${labels}]`, defaultChoice);
|
|
123
|
+
const match = choices.find(choice => choice.value === answer);
|
|
124
|
+
if (match) {
|
|
125
|
+
return match.value;
|
|
126
|
+
}
|
|
127
|
+
console.log(`Choose one of: ${labels}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function confirm(question, defaultValue = false) {
|
|
132
|
+
const answer = await prompt(`${question} [${defaultValue ? 'Y/n' : 'y/N'}]`, defaultValue ? 'y' : 'n');
|
|
133
|
+
return ['y', 'yes'].includes(answer.toLowerCase());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function validateId(id) {
|
|
137
|
+
return /^[a-z0-9-]+\/[a-z0-9-]+$/.test(id);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function applyPreset(options) {
|
|
141
|
+
const preset = options.preset || 'frontend';
|
|
142
|
+
const features = {
|
|
143
|
+
frontend: true,
|
|
144
|
+
apiRoutes: false,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
if (preset === 'minimal') {
|
|
148
|
+
features.frontend = false;
|
|
149
|
+
features.apiRoutes = false;
|
|
150
|
+
} else if (preset === 'backend') {
|
|
151
|
+
features.frontend = false;
|
|
152
|
+
features.apiRoutes = true;
|
|
153
|
+
} else if (preset === 'full') {
|
|
154
|
+
features.frontend = true;
|
|
155
|
+
features.apiRoutes = true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (options.withFrontend === false) {
|
|
159
|
+
features.frontend = false;
|
|
160
|
+
} else if (options.withFrontend === true && options.preset === null) {
|
|
161
|
+
features.frontend = true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (options.withApiRoutes === true) {
|
|
165
|
+
features.apiRoutes = true;
|
|
166
|
+
} else if (options.withApiRoutes === false && options.preset === null) {
|
|
167
|
+
features.apiRoutes = false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
options.withFrontend = features.frontend;
|
|
171
|
+
options.withApiRoutes = features.apiRoutes;
|
|
172
|
+
options.preset = preset;
|
|
173
|
+
|
|
174
|
+
return options;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function resolveOptions(options) {
|
|
178
|
+
if (!options.id && !isInteractive()) {
|
|
179
|
+
console.error('Error: extension id is required in non-interactive mode.');
|
|
180
|
+
usage(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (options.id && !validateId(options.id)) {
|
|
184
|
+
console.error('Error: extension id must use vendor/name format with lowercase letters, numbers, and hyphens.');
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!options.id) {
|
|
189
|
+
console.log('Notur extension setup');
|
|
190
|
+
while (!options.id) {
|
|
191
|
+
const id = await prompt('Extension id', 'acme/red-button');
|
|
192
|
+
if (validateId(id)) {
|
|
193
|
+
options.id = id;
|
|
194
|
+
} else {
|
|
195
|
+
console.log('Use vendor/name format with lowercase letters, numbers, and hyphens.');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const [, name] = options.id.split('/');
|
|
201
|
+
if (!options.displayName && isInteractive()) {
|
|
202
|
+
options.displayName = await prompt('Display name', displayName(name));
|
|
203
|
+
}
|
|
204
|
+
if (!options.description && isInteractive()) {
|
|
205
|
+
options.description = await prompt('Description', 'A Notur extension');
|
|
206
|
+
}
|
|
207
|
+
if (!options.preset && isInteractive()) {
|
|
208
|
+
options.preset = await select('Preset', [
|
|
209
|
+
{ value: 'frontend' },
|
|
210
|
+
{ value: 'backend' },
|
|
211
|
+
{ value: 'full' },
|
|
212
|
+
{ value: 'minimal' },
|
|
213
|
+
], 'frontend');
|
|
214
|
+
}
|
|
215
|
+
if (isInteractive() && ['frontend', 'full'].includes(options.preset) && options.withFrontend !== false) {
|
|
216
|
+
options.slot = await prompt('Initial frontend slot', options.slot);
|
|
217
|
+
}
|
|
218
|
+
if (!options.packageManager && isInteractive() && options.withFrontend !== false && options.preset !== 'minimal' && options.preset !== 'backend') {
|
|
219
|
+
options.packageManager = await select('Package manager', [
|
|
220
|
+
{ value: 'npm' },
|
|
221
|
+
{ value: 'pnpm' },
|
|
222
|
+
{ value: 'yarn' },
|
|
223
|
+
{ value: 'bun' },
|
|
224
|
+
], 'npm');
|
|
225
|
+
}
|
|
226
|
+
if (options.createEnv === null && isInteractive()) {
|
|
227
|
+
options.createEnv = await confirm('Create .env from .env.example?', false);
|
|
228
|
+
}
|
|
229
|
+
if (options.install === null && isInteractive() && options.withFrontend !== false && options.preset !== 'minimal' && options.preset !== 'backend') {
|
|
230
|
+
options.install = await confirm('Install frontend dependencies now?', false);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return applyPreset(options);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function studly(value) {
|
|
237
|
+
return value
|
|
238
|
+
.split('-')
|
|
239
|
+
.filter(Boolean)
|
|
240
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
241
|
+
.join('');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function displayName(value) {
|
|
245
|
+
return value
|
|
246
|
+
.split('-')
|
|
247
|
+
.filter(Boolean)
|
|
248
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
249
|
+
.join(' ');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function libraryName(id) {
|
|
253
|
+
return id
|
|
254
|
+
.split(/[\/-]/)
|
|
255
|
+
.filter(Boolean)
|
|
256
|
+
.map(studly)
|
|
257
|
+
.join('');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function yamlString(value) {
|
|
261
|
+
return String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function writeFile(filePath, content) {
|
|
265
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
266
|
+
fs.writeFileSync(filePath, content);
|
|
267
|
+
console.log(` created ${path.relative(process.cwd(), filePath)}`);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function ensureTarget(target, force) {
|
|
271
|
+
if (!fs.existsSync(target)) {
|
|
272
|
+
fs.mkdirSync(target, { recursive: true });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const entries = fs.readdirSync(target);
|
|
277
|
+
if (entries.length > 0 || !force) {
|
|
278
|
+
console.error(`Error: target directory already exists: ${target}`);
|
|
279
|
+
console.error('Use --force only for an existing empty directory.');
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function manifestTemplate({ id, vendor, name, className, namespace, frontend, apiRoutes, display, description }) {
|
|
285
|
+
const frontendSection = frontend
|
|
286
|
+
? `
|
|
287
|
+
frontend:
|
|
288
|
+
bundle: "resources/frontend/dist/extension.js"
|
|
289
|
+
`
|
|
290
|
+
: '';
|
|
291
|
+
const backendSection = apiRoutes
|
|
292
|
+
? `
|
|
293
|
+
backend:
|
|
294
|
+
routes:
|
|
295
|
+
api-client: "src/routes/api-client.php"
|
|
296
|
+
`
|
|
297
|
+
: '';
|
|
298
|
+
|
|
299
|
+
return `notur: "1.0"
|
|
300
|
+
id: "${id}"
|
|
301
|
+
name: "${yamlString(display)}"
|
|
302
|
+
version: "1.0.0"
|
|
303
|
+
description: "${yamlString(description)}"
|
|
304
|
+
license: "MIT"
|
|
305
|
+
|
|
306
|
+
requires:
|
|
307
|
+
notur: "^1.0"
|
|
308
|
+
pterodactyl: "^1.12"
|
|
309
|
+
php: "^8.2"
|
|
310
|
+
|
|
311
|
+
entrypoint: "${namespace.replace(/\\/g, '\\\\')}\\\\${className}"
|
|
312
|
+
autoload:
|
|
313
|
+
psr-4:
|
|
314
|
+
"${namespace.replace(/\\/g, '\\\\')}\\\\": "src/"
|
|
315
|
+
${backendSection}
|
|
316
|
+
${frontendSection}`;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function phpTemplate({ namespace, className }) {
|
|
320
|
+
return `<?php
|
|
321
|
+
|
|
322
|
+
declare(strict_types=1);
|
|
323
|
+
|
|
324
|
+
namespace ${namespace};
|
|
325
|
+
|
|
326
|
+
use Notur\\Support\\NoturExtension;
|
|
327
|
+
|
|
328
|
+
class ${className} extends NoturExtension
|
|
329
|
+
{
|
|
330
|
+
}
|
|
331
|
+
`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function apiRouteTemplate() {
|
|
335
|
+
return `<?php
|
|
336
|
+
|
|
337
|
+
use Illuminate\\Support\\Facades\\Route;
|
|
338
|
+
|
|
339
|
+
Route::get('/ping', function () {
|
|
340
|
+
return response()->json([
|
|
341
|
+
'message' => 'pong',
|
|
342
|
+
]);
|
|
343
|
+
});
|
|
344
|
+
`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function frontendTemplate({ id, slot }) {
|
|
348
|
+
return `import * as React from 'react';
|
|
349
|
+
import { createExtension } from '@notur/sdk';
|
|
350
|
+
|
|
351
|
+
const ExampleButton: React.FC = () => {
|
|
352
|
+
return (
|
|
353
|
+
<button
|
|
354
|
+
style={{
|
|
355
|
+
background: '#dc2626',
|
|
356
|
+
color: '#fff',
|
|
357
|
+
border: 0,
|
|
358
|
+
borderRadius: '6px',
|
|
359
|
+
padding: '8px 12px',
|
|
360
|
+
fontWeight: 600,
|
|
361
|
+
cursor: 'pointer',
|
|
362
|
+
}}
|
|
363
|
+
onClick={() => alert('Hello from Notur')}
|
|
364
|
+
>
|
|
365
|
+
Red Button
|
|
366
|
+
</button>
|
|
367
|
+
);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
createExtension({
|
|
371
|
+
id: '${id}',
|
|
372
|
+
slots: [
|
|
373
|
+
{
|
|
374
|
+
slot: '${slot}',
|
|
375
|
+
component: ExampleButton,
|
|
376
|
+
order: 10,
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
});
|
|
380
|
+
`;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function packageTemplate(id) {
|
|
384
|
+
return `${JSON.stringify({
|
|
385
|
+
name: id.replace('/', '-'),
|
|
386
|
+
version: '1.0.0',
|
|
387
|
+
private: true,
|
|
388
|
+
scripts: {
|
|
389
|
+
build: 'webpack-cli --mode production --config webpack.config.js',
|
|
390
|
+
dev: 'webpack-cli --mode development --watch --config webpack.config.js',
|
|
391
|
+
pack: 'notur-pack',
|
|
392
|
+
push: 'notur-push',
|
|
393
|
+
},
|
|
394
|
+
peerDependencies: {
|
|
395
|
+
react: '^16.14.0',
|
|
396
|
+
'react-dom': '^16.14.0',
|
|
397
|
+
},
|
|
398
|
+
devDependencies: {
|
|
399
|
+
'@notur/sdk': '^1.4.5',
|
|
400
|
+
'@types/react': '^16.14.0',
|
|
401
|
+
'@types/react-dom': '^16.9.0',
|
|
402
|
+
react: '^16.14.0',
|
|
403
|
+
'react-dom': '^16.14.0',
|
|
404
|
+
'ts-loader': '^9.5.0',
|
|
405
|
+
typescript: '^5.3.0',
|
|
406
|
+
webpack: '^5.90.0',
|
|
407
|
+
'webpack-cli': '^6.0.0',
|
|
408
|
+
},
|
|
409
|
+
}, null, 2)}
|
|
410
|
+
`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function tsconfigTemplate() {
|
|
414
|
+
return `{
|
|
415
|
+
"compilerOptions": {
|
|
416
|
+
"target": "ES2019",
|
|
417
|
+
"module": "ESNext",
|
|
418
|
+
"moduleResolution": "Node",
|
|
419
|
+
"jsx": "react",
|
|
420
|
+
"strict": true,
|
|
421
|
+
"esModuleInterop": true,
|
|
422
|
+
"skipLibCheck": true,
|
|
423
|
+
"forceConsistentCasingInFileNames": true
|
|
424
|
+
},
|
|
425
|
+
"include": ["resources/frontend/src/**/*"]
|
|
426
|
+
}
|
|
427
|
+
`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function webpackTemplate(libName) {
|
|
431
|
+
return `const path = require('path');
|
|
432
|
+
const base = require('@notur/sdk/webpack.extension.config');
|
|
433
|
+
|
|
434
|
+
module.exports = {
|
|
435
|
+
...base,
|
|
436
|
+
entry: './resources/frontend/src/index.tsx',
|
|
437
|
+
output: {
|
|
438
|
+
...base.output,
|
|
439
|
+
filename: 'extension.js',
|
|
440
|
+
path: path.resolve(__dirname, 'resources/frontend/dist'),
|
|
441
|
+
library: {
|
|
442
|
+
...base.output.library,
|
|
443
|
+
name: '__NOTUR_EXT_${libName}__',
|
|
444
|
+
type: 'umd',
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
`;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function readmeTemplate(id) {
|
|
452
|
+
return `# ${id}
|
|
453
|
+
|
|
454
|
+
Development:
|
|
455
|
+
|
|
456
|
+
\`\`\`bash
|
|
457
|
+
npm install
|
|
458
|
+
npm run build
|
|
459
|
+
npx notur-pack
|
|
460
|
+
\`\`\`
|
|
461
|
+
|
|
462
|
+
Remote push to a Notur-enabled panel:
|
|
463
|
+
|
|
464
|
+
\`\`\`bash
|
|
465
|
+
cp .env.example .env
|
|
466
|
+
npm run push
|
|
467
|
+
\`\`\`
|
|
468
|
+
|
|
469
|
+
Or pass values directly:
|
|
470
|
+
|
|
471
|
+
\`\`\`bash
|
|
472
|
+
npx notur-push --host https://panel.example.com --key notur_xxx
|
|
473
|
+
\`\`\`
|
|
474
|
+
`;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function installCommand(packageManager) {
|
|
478
|
+
if (packageManager === 'pnpm') return ['pnpm', ['install']];
|
|
479
|
+
if (packageManager === 'yarn') return ['yarn', ['install']];
|
|
480
|
+
if (packageManager === 'bun') return ['bun', ['install']];
|
|
481
|
+
return ['npm', ['install']];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function runScriptCommand(packageManager, script) {
|
|
485
|
+
if (packageManager === 'bun') return `bun run ${script}`;
|
|
486
|
+
if (packageManager === 'pnpm') return `pnpm run ${script}`;
|
|
487
|
+
if (packageManager === 'yarn') return `yarn ${script}`;
|
|
488
|
+
return `npm run ${script}`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function runInstall(target, packageManager) {
|
|
492
|
+
const [command, args] = installCommand(packageManager || 'npm');
|
|
493
|
+
console.log(`\nRunning ${command} ${args.join(' ')}...`);
|
|
494
|
+
const result = spawnSync(command, args, {
|
|
495
|
+
cwd: target,
|
|
496
|
+
stdio: 'inherit',
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
if (result.status !== 0) {
|
|
500
|
+
console.warn(`Dependency install failed. Run ${command} ${args.join(' ')} manually in ${target}.`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function main() {
|
|
505
|
+
const options = await resolveOptions(parseArgs());
|
|
506
|
+
const [vendor, name] = options.id.split('/');
|
|
507
|
+
const namespace = `${studly(vendor)}\\${studly(name)}`;
|
|
508
|
+
const className = `${studly(name)}Extension`;
|
|
509
|
+
const target = path.resolve(options.path, name);
|
|
510
|
+
|
|
511
|
+
ensureTarget(target, options.force);
|
|
512
|
+
|
|
513
|
+
console.log(`Scaffolding ${options.id} in ${target}`);
|
|
514
|
+
|
|
515
|
+
writeFile(path.join(target, 'extension.yaml'), manifestTemplate({
|
|
516
|
+
id: options.id,
|
|
517
|
+
vendor,
|
|
518
|
+
name,
|
|
519
|
+
namespace,
|
|
520
|
+
className,
|
|
521
|
+
frontend: options.withFrontend,
|
|
522
|
+
apiRoutes: options.withApiRoutes,
|
|
523
|
+
display: options.displayName || displayName(name),
|
|
524
|
+
description: options.description || 'A Notur extension',
|
|
525
|
+
}));
|
|
526
|
+
writeFile(path.join(target, 'src', `${className}.php`), phpTemplate({ namespace, className }));
|
|
527
|
+
if (options.withApiRoutes) {
|
|
528
|
+
writeFile(path.join(target, 'src/routes/api-client.php'), apiRouteTemplate());
|
|
529
|
+
}
|
|
530
|
+
writeFile(path.join(target, 'README.md'), readmeTemplate(options.id));
|
|
531
|
+
writeFile(path.join(target, '.env.example'), `NOTUR_HOST=https://panel.example.com
|
|
532
|
+
NOTUR_PUSH_KEY=notur_xxx
|
|
533
|
+
`);
|
|
534
|
+
if (options.createEnv) {
|
|
535
|
+
writeFile(path.join(target, '.env'), `NOTUR_HOST=https://panel.example.com
|
|
536
|
+
NOTUR_PUSH_KEY=notur_xxx
|
|
537
|
+
`);
|
|
538
|
+
}
|
|
539
|
+
writeFile(path.join(target, '.gitignore'), `node_modules/
|
|
540
|
+
vendor/
|
|
541
|
+
resources/frontend/dist/
|
|
542
|
+
.env
|
|
543
|
+
*.notur
|
|
544
|
+
*.notur.sha256
|
|
545
|
+
*.notur.sig
|
|
546
|
+
`);
|
|
547
|
+
|
|
548
|
+
if (options.withFrontend) {
|
|
549
|
+
writeFile(path.join(target, 'resources/frontend/src/index.tsx'), frontendTemplate({
|
|
550
|
+
id: options.id,
|
|
551
|
+
slot: options.slot,
|
|
552
|
+
}));
|
|
553
|
+
writeFile(path.join(target, 'package.json'), packageTemplate(options.id));
|
|
554
|
+
writeFile(path.join(target, 'tsconfig.json'), tsconfigTemplate());
|
|
555
|
+
writeFile(path.join(target, 'webpack.config.js'), webpackTemplate(libraryName(options.id)));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (options.install && options.withFrontend) {
|
|
559
|
+
runInstall(target, options.packageManager);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
console.log('\nNext steps:');
|
|
563
|
+
if (options.withFrontend) {
|
|
564
|
+
console.log(` cd ${target}`);
|
|
565
|
+
if (!options.install) {
|
|
566
|
+
const [command, args] = installCommand(options.packageManager || 'npm');
|
|
567
|
+
console.log(` ${command} ${args.join(' ')}`);
|
|
568
|
+
}
|
|
569
|
+
console.log(` ${runScriptCommand(options.packageManager || 'npm', 'build')}`);
|
|
570
|
+
console.log(' npx notur-pack');
|
|
571
|
+
} else {
|
|
572
|
+
console.log(` cd ${target}`);
|
|
573
|
+
console.log(' npx notur-pack');
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
main().catch(error => {
|
|
578
|
+
console.error(error?.message || String(error));
|
|
579
|
+
process.exit(1);
|
|
580
|
+
});
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
function parseArgs() {
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const options = {
|
|
11
|
+
path: '.',
|
|
12
|
+
archive: null,
|
|
13
|
+
host: null,
|
|
14
|
+
key: null,
|
|
15
|
+
envFile: null,
|
|
16
|
+
endpoint: '/api/notur/dev/push',
|
|
17
|
+
force: true,
|
|
18
|
+
noBuild: false,
|
|
19
|
+
keepArchive: false,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < args.length; i++) {
|
|
23
|
+
const arg = args[i];
|
|
24
|
+
if (arg === '--archive') {
|
|
25
|
+
options.archive = args[++i];
|
|
26
|
+
} else if (arg === '--host') {
|
|
27
|
+
options.host = args[++i];
|
|
28
|
+
} else if (arg === '--key') {
|
|
29
|
+
options.key = args[++i];
|
|
30
|
+
} else if (arg === '--env-file') {
|
|
31
|
+
options.envFile = args[++i];
|
|
32
|
+
} else if (arg === '--endpoint') {
|
|
33
|
+
options.endpoint = args[++i];
|
|
34
|
+
} else if (arg === '--no-force') {
|
|
35
|
+
options.force = false;
|
|
36
|
+
} else if (arg === '--no-build') {
|
|
37
|
+
options.noBuild = true;
|
|
38
|
+
} else if (arg === '--keep-archive') {
|
|
39
|
+
options.keepArchive = true;
|
|
40
|
+
} else if (arg === '--help' || arg === '-h') {
|
|
41
|
+
usage(0);
|
|
42
|
+
} else if (!arg.startsWith('-')) {
|
|
43
|
+
options.path = arg;
|
|
44
|
+
} else {
|
|
45
|
+
console.error(`Unknown argument: ${arg}`);
|
|
46
|
+
usage(1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return options;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function usage(code) {
|
|
54
|
+
console.log(`Usage:
|
|
55
|
+
npx notur-push [path] --host https://panel.example.com --key notur_xxx
|
|
56
|
+
npx @notur/sdk push [path] --host https://panel.example.com --key notur_xxx
|
|
57
|
+
|
|
58
|
+
Options:
|
|
59
|
+
--archive <file> Upload an existing .notur archive instead of packing
|
|
60
|
+
--host <url> Remote Pterodactyl panel URL
|
|
61
|
+
--key <token> Notur remote push token
|
|
62
|
+
--env-file <file> Load values from a custom env file
|
|
63
|
+
--endpoint <path> Remote push endpoint (default: /api/notur/dev/push)
|
|
64
|
+
--no-build Skip npm/yarn/pnpm/bun build before packing
|
|
65
|
+
--no-force Do not overwrite an already installed extension
|
|
66
|
+
--keep-archive Keep the temporary archive generated for this push`);
|
|
67
|
+
process.exit(code);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function loadManifest(dir) {
|
|
71
|
+
const manifestPath = path.join(dir, 'extension.yaml');
|
|
72
|
+
if (!fs.existsSync(manifestPath)) {
|
|
73
|
+
return { id: path.basename(dir), version: 'dev' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const raw = fs.readFileSync(manifestPath, 'utf8');
|
|
77
|
+
const id = raw.match(/^id:\s*["']?([^"'\n]+)["']?/m)?.[1]?.trim();
|
|
78
|
+
const version = raw.match(/^version:\s*["']?([^"'\n]+)["']?/m)?.[1]?.trim();
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
id: id || path.basename(dir),
|
|
82
|
+
version: version || 'dev',
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function detectPackageManager(dir) {
|
|
87
|
+
if (fs.existsSync(path.join(dir, 'bun.lockb')) || fs.existsSync(path.join(dir, 'bun.lock'))) return 'bun';
|
|
88
|
+
if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
89
|
+
if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn';
|
|
90
|
+
if (fs.existsSync(path.join(dir, 'package-lock.json')) || fs.existsSync(path.join(dir, 'package.json'))) return 'npm';
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildCommand(packageManager) {
|
|
95
|
+
switch (packageManager) {
|
|
96
|
+
case 'bun':
|
|
97
|
+
return ['bun', ['run', 'build']];
|
|
98
|
+
case 'pnpm':
|
|
99
|
+
return ['pnpm', ['run', 'build']];
|
|
100
|
+
case 'yarn':
|
|
101
|
+
return ['yarn', ['run', 'build']];
|
|
102
|
+
case 'npm':
|
|
103
|
+
return ['npm', ['run', 'build']];
|
|
104
|
+
default:
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function maybeBuild(dir, noBuild) {
|
|
110
|
+
if (noBuild || !fs.existsSync(path.join(dir, 'package.json'))) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
115
|
+
if (!packageJson.scripts || !packageJson.scripts.build) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const packageManager = detectPackageManager(dir);
|
|
120
|
+
const command = buildCommand(packageManager);
|
|
121
|
+
if (!command) {
|
|
122
|
+
console.warn('No supported package manager found; skipping build.');
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
console.log(`Running ${command[0]} ${command[1].join(' ')}...`);
|
|
127
|
+
const result = spawnSync(command[0], command[1], {
|
|
128
|
+
cwd: dir,
|
|
129
|
+
stdio: 'inherit',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (result.status !== 0) {
|
|
133
|
+
console.error('Build failed; aborting push.');
|
|
134
|
+
process.exit(result.status ?? 1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function packArchive(dir) {
|
|
139
|
+
const manifest = loadManifest(dir);
|
|
140
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'notur-push-'));
|
|
141
|
+
const archiveName = `${manifest.id.replace('/', '-')}-${manifest.version}.notur`;
|
|
142
|
+
const archivePath = path.join(tmpDir, archiveName);
|
|
143
|
+
const packScript = path.join(__dirname, 'notur-pack.js');
|
|
144
|
+
|
|
145
|
+
const result = spawnSync(process.execPath, [packScript, dir, '--output', archivePath], {
|
|
146
|
+
stdio: 'inherit',
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (result.status !== 0) {
|
|
150
|
+
console.error('Packaging failed; aborting push.');
|
|
151
|
+
process.exit(result.status ?? 1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { archivePath, tmpDir };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveUrl(host, endpoint, force) {
|
|
158
|
+
const base = host.replace(/\/+$/, '');
|
|
159
|
+
const pathPart = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
|
160
|
+
const url = new URL(`${base}${pathPart}`);
|
|
161
|
+
if (!force) {
|
|
162
|
+
url.searchParams.set('force', '0');
|
|
163
|
+
}
|
|
164
|
+
return url;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function pushArchive(options, archivePath) {
|
|
168
|
+
if (typeof fetch !== 'function' || typeof FormData !== 'function' || typeof Blob !== 'function') {
|
|
169
|
+
console.error('Error: notur-push requires Node.js 18+ for fetch/FormData support.');
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const url = resolveUrl(options.host, options.endpoint, options.force);
|
|
174
|
+
const data = fs.readFileSync(archivePath);
|
|
175
|
+
const form = new FormData();
|
|
176
|
+
form.append('extension', new Blob([data]), path.basename(archivePath));
|
|
177
|
+
|
|
178
|
+
const signaturePath = `${archivePath}.sig`;
|
|
179
|
+
if (fs.existsSync(signaturePath)) {
|
|
180
|
+
const signature = fs.readFileSync(signaturePath);
|
|
181
|
+
form.append('signature', new Blob([signature]), path.basename(signaturePath));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log(`Uploading ${path.basename(archivePath)} to ${url.origin}...`);
|
|
185
|
+
|
|
186
|
+
const response = await fetch(url, {
|
|
187
|
+
method: 'POST',
|
|
188
|
+
headers: {
|
|
189
|
+
Authorization: `Bearer ${options.key}`,
|
|
190
|
+
Accept: 'application/json',
|
|
191
|
+
},
|
|
192
|
+
body: form,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const text = await response.text();
|
|
196
|
+
let payload = null;
|
|
197
|
+
try {
|
|
198
|
+
payload = text ? JSON.parse(text) : null;
|
|
199
|
+
} catch {
|
|
200
|
+
payload = null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
console.error(`Remote push failed (${response.status}).`);
|
|
205
|
+
if (payload?.message) {
|
|
206
|
+
console.error(payload.message);
|
|
207
|
+
} else if (text) {
|
|
208
|
+
console.error(text);
|
|
209
|
+
}
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (payload) {
|
|
214
|
+
console.log(`Pushed ${payload.id || 'extension'} v${payload.version || 'unknown'}.`);
|
|
215
|
+
if (payload.output) {
|
|
216
|
+
console.log(payload.output.trim());
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
console.log('Push completed.');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function parseEnvValue(value) {
|
|
224
|
+
let parsed = value.trim();
|
|
225
|
+
if (
|
|
226
|
+
(parsed.startsWith('"') && parsed.endsWith('"')) ||
|
|
227
|
+
(parsed.startsWith("'") && parsed.endsWith("'"))
|
|
228
|
+
) {
|
|
229
|
+
parsed = parsed.slice(1, -1);
|
|
230
|
+
}
|
|
231
|
+
return parsed;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function loadEnvFile(filePath) {
|
|
235
|
+
if (!fs.existsSync(filePath)) {
|
|
236
|
+
return {};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const values = {};
|
|
240
|
+
const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
|
|
241
|
+
|
|
242
|
+
for (const line of lines) {
|
|
243
|
+
const trimmed = line.trim();
|
|
244
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const match = trimmed.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
249
|
+
if (!match) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
values[match[1]] = parseEnvValue(match[2]);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return values;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function resolvePushConfig(options, extensionPath) {
|
|
260
|
+
const envFile = options.envFile
|
|
261
|
+
? path.resolve(options.envFile)
|
|
262
|
+
: path.join(extensionPath, '.env');
|
|
263
|
+
const fileEnv = loadEnvFile(envFile);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
...options,
|
|
267
|
+
host: options.host || process.env.NOTUR_HOST || fileEnv.NOTUR_HOST || null,
|
|
268
|
+
key:
|
|
269
|
+
options.key ||
|
|
270
|
+
process.env.NOTUR_API_KEY ||
|
|
271
|
+
process.env.NOTUR_PUSH_KEY ||
|
|
272
|
+
fileEnv.NOTUR_API_KEY ||
|
|
273
|
+
fileEnv.NOTUR_PUSH_KEY ||
|
|
274
|
+
null,
|
|
275
|
+
endpoint: options.endpoint || fileEnv.NOTUR_ENDPOINT || '/api/notur/dev/push',
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function main() {
|
|
280
|
+
let options = parseArgs();
|
|
281
|
+
const extensionPath = path.resolve(options.path);
|
|
282
|
+
|
|
283
|
+
if (!fs.existsSync(extensionPath) || !fs.statSync(extensionPath).isDirectory()) {
|
|
284
|
+
console.error(`Error: extension path does not exist: ${extensionPath}`);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
options = resolvePushConfig(options, extensionPath);
|
|
289
|
+
|
|
290
|
+
if (!options.host) {
|
|
291
|
+
console.error('Error: --host is required, or set NOTUR_HOST in the environment or local .env.');
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (!options.key) {
|
|
296
|
+
console.error('Error: --key is required, or set NOTUR_PUSH_KEY / NOTUR_API_KEY in the environment or local .env.');
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (!options.archive) {
|
|
301
|
+
maybeBuild(extensionPath, options.noBuild);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let archivePath = options.archive ? path.resolve(options.archive) : null;
|
|
305
|
+
let tmpDir = null;
|
|
306
|
+
if (!archivePath) {
|
|
307
|
+
const packed = packArchive(extensionPath);
|
|
308
|
+
archivePath = packed.archivePath;
|
|
309
|
+
tmpDir = packed.tmpDir;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!fs.existsSync(archivePath)) {
|
|
313
|
+
console.error(`Error: archive does not exist: ${archivePath}`);
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
await pushArchive(options, archivePath);
|
|
319
|
+
} finally {
|
|
320
|
+
if (tmpDir && !options.keepArchive) {
|
|
321
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
322
|
+
} else if (tmpDir) {
|
|
323
|
+
console.log(`Kept archive at ${archivePath}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
main().catch(error => {
|
|
329
|
+
console.error(error?.message || String(error));
|
|
330
|
+
process.exit(1);
|
|
331
|
+
});
|
package/bin/notur.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
const commands = {
|
|
7
|
+
create: 'notur-create.js',
|
|
8
|
+
pack: 'notur-pack.js',
|
|
9
|
+
keygen: 'notur-keygen.js',
|
|
10
|
+
push: 'notur-push.js',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const [command, ...args] = process.argv.slice(2);
|
|
14
|
+
|
|
15
|
+
if (!command || command === '--help' || command === '-h') {
|
|
16
|
+
console.log(`Notur SDK CLI
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
notur create <vendor/name> [options]
|
|
20
|
+
notur pack [path] [options]
|
|
21
|
+
notur push [path] --host <url> --key <token>
|
|
22
|
+
notur keygen
|
|
23
|
+
|
|
24
|
+
Standalone bins are also available:
|
|
25
|
+
notur-create, notur-pack, notur-push, notur-keygen`);
|
|
26
|
+
process.exit(command ? 0 : 1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const script = commands[command];
|
|
30
|
+
if (!script) {
|
|
31
|
+
console.error(`Unknown command: ${command}`);
|
|
32
|
+
console.error('Run "notur --help" for usage.');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const result = spawnSync(process.execPath, [path.join(__dirname, script), ...args], {
|
|
37
|
+
stdio: 'inherit',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Red Button Example
|
|
2
|
+
|
|
3
|
+
A minimal Notur frontend extension that renders a red button in the server header.
|
|
4
|
+
|
|
5
|
+
## Local Build
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run build
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Package
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npx notur-pack
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Push To A Remote Panel
|
|
19
|
+
|
|
20
|
+
Create `.env` from `.env.example`, then run:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm run push
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The remote panel must have Notur remote push enabled:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
php artisan notur:remote-key
|
|
30
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
notur: "1.0"
|
|
2
|
+
id: "acme/red-button"
|
|
3
|
+
name: "Red Button"
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
description: "Adds a red button to the server header"
|
|
6
|
+
license: "MIT"
|
|
7
|
+
|
|
8
|
+
requires:
|
|
9
|
+
notur: "^1.0"
|
|
10
|
+
pterodactyl: "^1.12"
|
|
11
|
+
php: "^8.2"
|
|
12
|
+
|
|
13
|
+
entrypoint: "Acme\\RedButton\\RedButtonExtension"
|
|
14
|
+
autoload:
|
|
15
|
+
psr-4:
|
|
16
|
+
"Acme\\RedButton\\": "src/"
|
|
17
|
+
|
|
18
|
+
frontend:
|
|
19
|
+
bundle: "resources/frontend/dist/extension.js"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "acme-red-button",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "webpack-cli --mode production --config webpack.config.js",
|
|
7
|
+
"dev": "webpack-cli --mode development --watch --config webpack.config.js",
|
|
8
|
+
"pack": "notur-pack",
|
|
9
|
+
"push": "notur-push"
|
|
10
|
+
},
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"react": "^16.14.0",
|
|
13
|
+
"react-dom": "^16.14.0"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@notur/sdk": "^1.4.5",
|
|
17
|
+
"@types/react": "^16.14.0",
|
|
18
|
+
"@types/react-dom": "^16.9.0",
|
|
19
|
+
"react": "^16.14.0",
|
|
20
|
+
"react-dom": "^16.14.0",
|
|
21
|
+
"ts-loader": "^9.5.0",
|
|
22
|
+
"typescript": "^5.3.0",
|
|
23
|
+
"webpack": "^5.90.0",
|
|
24
|
+
"webpack-cli": "^6.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { createExtension } from '@notur/sdk';
|
|
3
|
+
|
|
4
|
+
const RedButton: React.FC = () => {
|
|
5
|
+
return (
|
|
6
|
+
<button
|
|
7
|
+
style={{
|
|
8
|
+
background: '#dc2626',
|
|
9
|
+
color: '#fff',
|
|
10
|
+
border: 0,
|
|
11
|
+
borderRadius: '6px',
|
|
12
|
+
padding: '8px 12px',
|
|
13
|
+
fontWeight: 600,
|
|
14
|
+
cursor: 'pointer',
|
|
15
|
+
}}
|
|
16
|
+
onClick={() => alert('Hello from Notur')}
|
|
17
|
+
>
|
|
18
|
+
Red Button
|
|
19
|
+
</button>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
createExtension({
|
|
24
|
+
id: 'acme/red-button',
|
|
25
|
+
slots: [
|
|
26
|
+
{
|
|
27
|
+
slot: 'server.header',
|
|
28
|
+
component: RedButton,
|
|
29
|
+
order: 10,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2019",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Node",
|
|
6
|
+
"jsx": "react",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true
|
|
11
|
+
},
|
|
12
|
+
"include": ["resources/frontend/src/**/*"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const base = require('@notur/sdk/webpack.extension.config');
|
|
3
|
+
|
|
4
|
+
module.exports = {
|
|
5
|
+
...base,
|
|
6
|
+
entry: './resources/frontend/src/index.tsx',
|
|
7
|
+
output: {
|
|
8
|
+
...base.output,
|
|
9
|
+
filename: 'extension.js',
|
|
10
|
+
path: path.resolve(__dirname, 'resources/frontend/dist'),
|
|
11
|
+
library: {
|
|
12
|
+
...base.output.library,
|
|
13
|
+
name: '__NOTUR_EXT_AcmeRedButton__',
|
|
14
|
+
type: 'umd',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@notur/sdk",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.6",
|
|
4
4
|
"description": "Notur Extension Developer SDK",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
"access": "public"
|
|
16
16
|
},
|
|
17
17
|
"bin": {
|
|
18
|
+
"sdk": "./bin/notur.js",
|
|
19
|
+
"notur": "./bin/notur.js",
|
|
20
|
+
"notur-create": "./bin/notur-create.js",
|
|
18
21
|
"notur-pack": "./bin/notur-pack.js",
|
|
19
|
-
"notur-keygen": "./bin/notur-keygen.js"
|
|
22
|
+
"notur-keygen": "./bin/notur-keygen.js",
|
|
23
|
+
"notur-push": "./bin/notur-push.js"
|
|
20
24
|
},
|
|
21
25
|
"keywords": [
|
|
22
26
|
"notur",
|
|
@@ -29,7 +33,8 @@
|
|
|
29
33
|
"dist/",
|
|
30
34
|
"bin/",
|
|
31
35
|
"webpack.extension.config.js",
|
|
32
|
-
"templates/"
|
|
36
|
+
"templates/",
|
|
37
|
+
"examples/"
|
|
33
38
|
],
|
|
34
39
|
"scripts": {
|
|
35
40
|
"build": "tsc",
|