@notegen/plugin-cli 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,398 @@
1
+ import { basename } from 'node:path';
2
+ import { createInterface } from 'node:readline/promises';
3
+ import { Command, CommanderError, Option } from 'commander';
4
+ import { EXIT_PROJECT_FAILURE, EXIT_SUCCESS, EXIT_UNSAFE_REFUSAL, EXIT_UNEXPECTED, EXIT_USAGE, } from './lib/constants.js';
5
+ import { diagnostic, diagnosticsFromError, fail, formatDiagnostic, isDiagnosticError, } from './lib/diagnostics.js';
6
+ import { createPluginProject, } from './lib/scaffold.js';
7
+ import { buildPluginProject } from './lib/project.js';
8
+ import { watchPluginProject } from './lib/watch.js';
9
+ import { generatePublisherKeys, packPluginProject, signPluginArchive, validatePluginTarget, } from './lib/tasks.js';
10
+ export const PLUGIN_CLI_VERSION = '0.1.0';
11
+ function writeLine(stream, value = '') {
12
+ stream.write(`${value}\n`);
13
+ }
14
+ function printJson(io, value) {
15
+ writeLine(io.stdout, JSON.stringify(value, null, 2));
16
+ }
17
+ function isUnsafeRefusal(error) {
18
+ if (!isDiagnosticError(error))
19
+ return false;
20
+ const refusalCodes = new Set([
21
+ 'create.target_invalid',
22
+ 'create.target_not_empty',
23
+ 'file.exists',
24
+ 'file.not_regular',
25
+ 'key.output-exists',
26
+ 'key.same-output',
27
+ 'output.unsafe',
28
+ 'path.outside_project',
29
+ 'path.symlink',
30
+ 'sign.same-output',
31
+ ]);
32
+ return error.diagnostics.some((item) => refusalCodes.has(item.code));
33
+ }
34
+ function titleFromDirectory(directory) {
35
+ const name = basename(directory).replace(/[-_]+/g, ' ').trim();
36
+ return name.length === 0
37
+ ? 'NoteGen Plugin'
38
+ : name.replace(/\b[a-z]/g, (letter) => letter.toUpperCase());
39
+ }
40
+ async function completeCreateOptions(directory, options) {
41
+ if (options.id && options.name)
42
+ return { id: options.id, name: options.name };
43
+ if (options.yes || options.json) {
44
+ if (!options.id) {
45
+ fail('create.id-required', '--id is required when --yes or --json disables interactive prompts');
46
+ }
47
+ return { id: options.id, name: options.name ?? titleFromDirectory(directory) };
48
+ }
49
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
50
+ fail('create.non-interactive', 'Missing --id or --name in a non-interactive terminal');
51
+ }
52
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
53
+ try {
54
+ const id = options.id ?? (await prompt.question('Plugin ID (for example com.example.my-plugin): ')).trim();
55
+ const fallbackName = titleFromDirectory(directory);
56
+ const nameInput = options.name ?? (await prompt.question(`Plugin name (${fallbackName}): `)).trim();
57
+ return { id, name: nameInput || fallbackName };
58
+ }
59
+ finally {
60
+ prompt.close();
61
+ }
62
+ }
63
+ function readPassphrase(environmentVariable) {
64
+ if (environmentVariable === undefined)
65
+ return undefined;
66
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$(?![\s\S])/u.test(environmentVariable)) {
67
+ fail('key.invalid-environment-variable', '--passphrase-env must name a valid environment variable');
68
+ }
69
+ const value = process.env[environmentVariable];
70
+ if (!value) {
71
+ fail('key.missing-passphrase', `Environment variable ${environmentVariable} is empty or unavailable`);
72
+ }
73
+ return value;
74
+ }
75
+ function addCompatibilityOptions(command) {
76
+ return command
77
+ .option('--api-version <version>', 'validate against a concrete NoteGen plugin API version')
78
+ .option('--app-version <version>', 'validate against a concrete NoteGen app version');
79
+ }
80
+ function addJsonOption(command) {
81
+ return command.option('--json', 'write a machine-readable JSON result');
82
+ }
83
+ function hasJsonOption(argv) {
84
+ const arguments_ = argv.slice(2);
85
+ const optionTerminator = arguments_.indexOf('--');
86
+ const options = optionTerminator === -1
87
+ ? arguments_
88
+ : arguments_.slice(0, optionTerminator);
89
+ return options.includes('--json');
90
+ }
91
+ function appCompatibilityStatus(appVersion) {
92
+ return appVersion === undefined
93
+ ? {
94
+ appCompatibilityChecked: false,
95
+ appCompatibilityNote: 'NoteGen app compatibility was not checked; pass --app-version to check minAppVersion.',
96
+ }
97
+ : {
98
+ appCompatibilityChecked: true,
99
+ appCompatibilityNote: `Compatibility checked against NoteGen ${appVersion}.`,
100
+ };
101
+ }
102
+ export function createCliProgram(io = { stdout: process.stdout, stderr: process.stderr }, options = {}) {
103
+ const program = new Command()
104
+ .name('notegen-plugin')
105
+ .description('Create, build, validate, package, sign, and verify NoteGen plugins.')
106
+ .version(PLUGIN_CLI_VERSION)
107
+ .exitOverride()
108
+ .configureOutput({
109
+ writeOut: (value) => {
110
+ if (options.jsonMode)
111
+ options.commanderOutput?.push(value);
112
+ else
113
+ io.stdout.write(value);
114
+ },
115
+ writeErr: (value) => {
116
+ if (!options.jsonMode)
117
+ io.stderr.write(value);
118
+ },
119
+ });
120
+ if (!options.jsonMode)
121
+ program.showHelpAfterError();
122
+ const createCommand = addJsonOption(program.command('create <directory>')
123
+ .description('create a new NoteGen plugin project')
124
+ .option('--id <plugin-id>', 'reverse-domain plugin ID')
125
+ .option('--name <name>', 'display name')
126
+ .option('--description <description>', 'short plugin description')
127
+ .addOption(new Option('--template <template>', 'starter template')
128
+ .choices(['command', 'editor-statistics'])
129
+ .default('command'))
130
+ .option('--min-app-version <version>', 'minimum NoteGen version', '0.37.0')
131
+ .option('--api-version <range>', 'supported plugin API range', '^0.1.0')
132
+ .addOption(new Option('--package-manager <manager>', 'generated project package manager')
133
+ .choices(['pnpm', 'npm'])
134
+ .default('pnpm'))
135
+ .option('--install', 'install generated project dependencies')
136
+ .option('--yes', 'disable prompts; --id is required'));
137
+ createCommand.action(async (directory) => {
138
+ const options = createCommand.opts();
139
+ const identity = await completeCreateOptions(directory, options);
140
+ const created = await createPluginProject({
141
+ directory,
142
+ ...identity,
143
+ ...(options.description === undefined ? {} : { description: options.description }),
144
+ template: options.template,
145
+ minAppVersion: options.minAppVersion,
146
+ apiVersion: options.apiVersion,
147
+ packageManager: options.packageManager,
148
+ install: options.install,
149
+ ...(options.json ? { installOutput: io.stderr } : {}),
150
+ });
151
+ if (options.json) {
152
+ printJson(io, { ok: true, command: 'create', ...created });
153
+ return;
154
+ }
155
+ writeLine(io.stdout, `Created NoteGen plugin project at ${created.directory}`);
156
+ writeLine(io.stdout, created.installed
157
+ ? `Dependencies installed with ${created.packageManager}.`
158
+ : `Run ${created.packageManager} install before building.`);
159
+ });
160
+ const validateCommand = addJsonOption(addCompatibilityOptions(program.command('validate [path]')
161
+ .description('validate a source project, development directory, or plugin archive')
162
+ .option('--public-key <file>', 'publisher public-key JSON or raw Base64 file')
163
+ .option('--require-signature', 'reject unsigned packages')));
164
+ validateCommand.action(async (path) => {
165
+ const options = validateCommand.opts();
166
+ const result = await validatePluginTarget({
167
+ ...(path === undefined ? {} : { target: path }),
168
+ ...(options.apiVersion === undefined ? {} : { apiVersion: options.apiVersion }),
169
+ ...(options.appVersion === undefined ? {} : { appVersion: options.appVersion }),
170
+ ...(options.publicKey === undefined ? {} : { publicKeyPath: options.publicKey }),
171
+ ...(options.requireSignature === undefined ? {} : { requireSignature: options.requireSignature }),
172
+ });
173
+ if (options.json) {
174
+ printJson(io, { ok: true, command: 'validate', ...result });
175
+ return;
176
+ }
177
+ writeLine(io.stdout, `Valid ${result.kind}: ${result.manifest.id}@${result.manifest.version}`);
178
+ writeLine(io.stdout, result.appCompatibilityNote);
179
+ if (result.signed) {
180
+ writeLine(io.stdout, result.signatureVerified
181
+ ? 'Publisher signature verified.'
182
+ : 'Signature is structurally valid; pass --public-key to verify its identity.');
183
+ }
184
+ });
185
+ const devCommand = addJsonOption(addCompatibilityOptions(program.command('dev [directory]')
186
+ .description('watch source changes and atomically replace the development package after successful builds')));
187
+ devCommand.action(async (directory) => {
188
+ const options = devCommand.opts();
189
+ const controller = new AbortController();
190
+ const stop = () => controller.abort();
191
+ process.once('SIGINT', stop);
192
+ process.once('SIGTERM', stop);
193
+ try {
194
+ await watchPluginProject({
195
+ ...(directory === undefined ? {} : { directory }),
196
+ ...(options.apiVersion === undefined ? {} : { apiVersion: options.apiVersion }),
197
+ ...(options.appVersion === undefined ? {} : { appVersion: options.appVersion }),
198
+ signal: controller.signal,
199
+ onBuilt: built => {
200
+ const result = { ok: true, command: 'dev', pluginId: built.manifest.id, outputDirectory: built.outputDirectory };
201
+ if (options.json)
202
+ printJson(io, result);
203
+ else
204
+ writeLine(io.stdout, `Built ${built.manifest.id}. Watching source changes. Import and enable auto-reload for: ${built.outputDirectory}`);
205
+ },
206
+ onError: error => {
207
+ const message = error instanceof Error ? error.message : String(error);
208
+ if (options.json)
209
+ printJson(io, { ok: false, command: 'dev', message });
210
+ else
211
+ writeLine(io.stderr, `${message}\nBuild did not finish cleanly; waiting for source changes. See the diagnostic for output recovery details.`);
212
+ },
213
+ });
214
+ }
215
+ finally {
216
+ process.removeListener('SIGINT', stop);
217
+ process.removeListener('SIGTERM', stop);
218
+ }
219
+ });
220
+ const buildCommand = addJsonOption(addCompatibilityOptions(program.command('build [directory]')
221
+ .description('bundle a plugin into an importable development directory')));
222
+ buildCommand.action(async (directory) => {
223
+ const options = buildCommand.opts();
224
+ const built = await buildPluginProject({
225
+ ...(directory === undefined ? {} : { directory }),
226
+ ...(options.apiVersion === undefined ? {} : { apiVersion: options.apiVersion }),
227
+ ...(options.appVersion === undefined ? {} : { appVersion: options.appVersion }),
228
+ });
229
+ const result = {
230
+ ok: true,
231
+ command: 'build',
232
+ pluginId: built.manifest.id,
233
+ version: built.manifest.version,
234
+ outputDirectory: built.outputDirectory,
235
+ ...appCompatibilityStatus(options.appVersion),
236
+ };
237
+ if (options.json)
238
+ printJson(io, result);
239
+ else {
240
+ writeLine(io.stdout, `Built ${result.pluginId}@${result.version}`);
241
+ writeLine(io.stdout, `Development import directory: ${result.outputDirectory}`);
242
+ writeLine(io.stdout, result.appCompatibilityNote);
243
+ }
244
+ });
245
+ const packCommand = addJsonOption(addCompatibilityOptions(program.command('pack [directory]')
246
+ .description('build and create an unsigned .notegen-plugin archive')
247
+ .option('--output <file>', 'unsigned archive output path')
248
+ .option('--force', 'overwrite an existing output archive')));
249
+ packCommand.action(async (directory) => {
250
+ const options = packCommand.opts();
251
+ const result = await packPluginProject({
252
+ ...(directory === undefined ? {} : { directory }),
253
+ ...(options.output === undefined ? {} : { output: options.output }),
254
+ ...(options.apiVersion === undefined ? {} : { apiVersion: options.apiVersion }),
255
+ ...(options.appVersion === undefined ? {} : { appVersion: options.appVersion }),
256
+ force: options.force,
257
+ });
258
+ const compatibility = appCompatibilityStatus(options.appVersion);
259
+ if (options.json)
260
+ printJson(io, { ok: true, command: 'pack', ...result, ...compatibility });
261
+ else {
262
+ writeLine(io.stdout, `Packed unsigned ${result.pluginId}@${result.version}`);
263
+ writeLine(io.stdout, `${result.path} (${result.sha256})`);
264
+ writeLine(io.stdout, 'Signing is a separate step; no private key was accessed.');
265
+ writeLine(io.stdout, compatibility.appCompatibilityNote);
266
+ }
267
+ });
268
+ const keygenCommand = addJsonOption(program.command('keygen')
269
+ .description('generate an Ed25519 publisher key pair')
270
+ .option('--output <directory>', 'key output directory (default: .notegen/keys)')
271
+ .option('--private-key <file>', 'private-key PEM output path')
272
+ .option('--public-key <file>', 'public-key JSON output path')
273
+ .option('--passphrase-env <name>', 'encrypt the private key using this environment variable')
274
+ .option('--force', 'overwrite both existing key files'));
275
+ keygenCommand.action(async () => {
276
+ const options = keygenCommand.opts();
277
+ const passphrase = readPassphrase(options.passphraseEnv);
278
+ const result = await generatePublisherKeys({
279
+ ...(options.output === undefined ? {} : { directory: options.output }),
280
+ ...(options.privateKey === undefined ? {} : { privateKeyPath: options.privateKey }),
281
+ ...(options.publicKey === undefined ? {} : { publicKeyPath: options.publicKey }),
282
+ ...(passphrase === undefined ? {} : { passphrase }),
283
+ force: options.force,
284
+ });
285
+ if (options.json)
286
+ printJson(io, { ok: true, command: 'keygen', ...result });
287
+ else {
288
+ writeLine(io.stdout, `Generated publisher key ${result.keyId}`);
289
+ writeLine(io.stdout, `Private key: ${result.privateKeyPath}`);
290
+ writeLine(io.stdout, `Public key: ${result.publicKeyPath}`);
291
+ writeLine(io.stdout, 'The private key contents were not printed. Keep that file offline.');
292
+ }
293
+ });
294
+ const signCommand = addJsonOption(addCompatibilityOptions(program.command('sign <unsigned-archive>')
295
+ .description('sign an already-built unsigned archive without executing or compiling it')
296
+ .requiredOption('--private-key <file>', 'publisher PKCS#8 PEM private key')
297
+ .option('--output <file>', 'signed archive output path')
298
+ .option('--passphrase-env <name>', 'read the private-key passphrase from this environment variable')
299
+ .option('--force', 'overwrite an existing signed archive')));
300
+ signCommand.action(async (archive) => {
301
+ const options = signCommand.opts();
302
+ const passphrase = readPassphrase(options.passphraseEnv);
303
+ const result = await signPluginArchive({
304
+ archive,
305
+ privateKeyPath: options.privateKey,
306
+ ...(options.output === undefined ? {} : { output: options.output }),
307
+ ...(passphrase === undefined ? {} : { passphrase }),
308
+ ...(options.apiVersion === undefined ? {} : { apiVersion: options.apiVersion }),
309
+ ...(options.appVersion === undefined ? {} : { appVersion: options.appVersion }),
310
+ force: options.force,
311
+ });
312
+ const compatibility = appCompatibilityStatus(options.appVersion);
313
+ if (options.json)
314
+ printJson(io, { ok: true, command: 'sign', ...result, ...compatibility });
315
+ else {
316
+ writeLine(io.stdout, `Signed ${result.pluginId}@${result.version} with ${result.keyId}`);
317
+ writeLine(io.stdout, `${result.path} (${result.sha256})`);
318
+ writeLine(io.stdout, compatibility.appCompatibilityNote);
319
+ }
320
+ });
321
+ const verifyCommand = addJsonOption(addCompatibilityOptions(program.command('verify <path>')
322
+ .description('verify a complete package directory or archive without executing plugin code')
323
+ .option('--public-key <file>', 'publisher public-key JSON or raw Base64 file')
324
+ .option('--require-signature', 'reject unsigned development packages')));
325
+ verifyCommand.action(async (path) => {
326
+ const options = verifyCommand.opts();
327
+ const result = await validatePluginTarget({
328
+ target: path,
329
+ completeDirectory: true,
330
+ ...(options.apiVersion === undefined ? {} : { apiVersion: options.apiVersion }),
331
+ ...(options.appVersion === undefined ? {} : { appVersion: options.appVersion }),
332
+ ...(options.publicKey === undefined ? {} : { publicKeyPath: options.publicKey }),
333
+ ...(options.requireSignature === undefined ? {} : { requireSignature: options.requireSignature }),
334
+ });
335
+ if (result.kind === 'project') {
336
+ fail('verify.incomplete-project', 'verify requires a built directory or archive; run build first', result.target);
337
+ }
338
+ if (options.json)
339
+ printJson(io, { ok: true, command: 'verify', ...result });
340
+ else {
341
+ writeLine(io.stdout, `Verified ${result.manifest.id}@${result.manifest.version}`);
342
+ writeLine(io.stdout, result.appCompatibilityNote);
343
+ writeLine(io.stdout, result.signed
344
+ ? result.signatureVerified
345
+ ? 'Integrity and publisher signature are valid.'
346
+ : 'Integrity and signature encoding are valid; publisher identity was not checked.'
347
+ : 'Integrity is valid; this development package is unsigned.');
348
+ }
349
+ });
350
+ return program;
351
+ }
352
+ export async function runCli(argv = process.argv, io = { stdout: process.stdout, stderr: process.stderr }) {
353
+ const jsonMode = hasJsonOption(argv);
354
+ const commanderOutput = [];
355
+ const program = createCliProgram(io, { jsonMode, commanderOutput });
356
+ try {
357
+ await program.parseAsync([...argv], { from: 'node' });
358
+ return EXIT_SUCCESS;
359
+ }
360
+ catch (error) {
361
+ if (error instanceof CommanderError) {
362
+ if (error.exitCode === 0) {
363
+ if (jsonMode) {
364
+ printJson(io, {
365
+ ok: true,
366
+ command: error.code === 'commander.version' ? 'version' : 'help',
367
+ output: commanderOutput.join('').trimEnd(),
368
+ });
369
+ }
370
+ return EXIT_SUCCESS;
371
+ }
372
+ if (jsonMode) {
373
+ printJson(io, {
374
+ ok: false,
375
+ diagnostics: [diagnostic({
376
+ code: 'cli.usage',
377
+ message: error.message.replace(/^error:\s*/u, ''),
378
+ })],
379
+ });
380
+ }
381
+ return EXIT_USAGE;
382
+ }
383
+ const diagnostics = diagnosticsFromError(error);
384
+ if (jsonMode)
385
+ printJson(io, { ok: false, diagnostics });
386
+ else
387
+ for (const item of diagnostics)
388
+ writeLine(io.stderr, formatDiagnostic(item));
389
+ if (isDiagnosticError(error)) {
390
+ return isUnsafeRefusal(error) ? EXIT_UNSAFE_REFUSAL : EXIT_PROJECT_FAILURE;
391
+ }
392
+ return EXIT_UNEXPECTED;
393
+ }
394
+ }
395
+ export async function runCreateCli(argv = process.argv, io = { stdout: process.stdout, stderr: process.stderr }) {
396
+ const forwarded = [argv[0] ?? 'node', argv[1] ?? 'create-notegen-plugin', 'create', ...argv.slice(2)];
397
+ return runCli(forwarded, io);
398
+ }
@@ -0,0 +1,13 @@
1
+ export * from './cli.js';
2
+ export * from './lib/archive.js';
3
+ export { DEVELOPMENT_OUTPUT_DIRECTORY, EXIT_INTERRUPTED, EXIT_PROJECT_FAILURE, EXIT_SUCCESS, EXIT_UNEXPECTED, EXIT_UNSAFE_REFUSAL, EXIT_USAGE, PACKAGE_EXTENSION, RELEASE_OUTPUT_DIRECTORY, UNSIGNED_PACKAGE_EXTENSION, } from './lib/constants.js';
4
+ export * from './lib/diagnostics.js';
5
+ export * from './lib/integrity.js';
6
+ export * from './lib/manifest.js';
7
+ export * from './lib/package.js';
8
+ export * from './lib/path-rules.js';
9
+ export * from './lib/project.js';
10
+ export * from './lib/scaffold.js';
11
+ export * from './lib/signing.js';
12
+ export * from './lib/tasks.js';
13
+ export * from './lib/watch.js';
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ export * from './cli.js';
2
+ export * from './lib/archive.js';
3
+ export { DEVELOPMENT_OUTPUT_DIRECTORY, EXIT_INTERRUPTED, EXIT_PROJECT_FAILURE, EXIT_SUCCESS, EXIT_UNEXPECTED, EXIT_UNSAFE_REFUSAL, EXIT_USAGE, PACKAGE_EXTENSION, RELEASE_OUTPUT_DIRECTORY, UNSIGNED_PACKAGE_EXTENSION, } from './lib/constants.js';
4
+ export * from './lib/diagnostics.js';
5
+ export * from './lib/integrity.js';
6
+ export * from './lib/manifest.js';
7
+ export * from './lib/package.js';
8
+ export * from './lib/path-rules.js';
9
+ export * from './lib/project.js';
10
+ export * from './lib/scaffold.js';
11
+ export * from './lib/signing.js';
12
+ export * from './lib/tasks.js';
13
+ export * from './lib/watch.js';
@@ -0,0 +1,17 @@
1
+ export interface PackageFile {
2
+ readonly path: string;
3
+ readonly bytes: Uint8Array;
4
+ }
5
+ export interface PackageArchive {
6
+ readonly files: ReadonlyMap<string, Buffer>;
7
+ readonly sha256: string;
8
+ readonly size: number;
9
+ }
10
+ export declare function readPackageArchive(path: string): Promise<PackageArchive>;
11
+ export declare function writePackageArchive(input: Iterable<PackageFile>, outputPath: string, options?: {
12
+ readonly force?: boolean;
13
+ }): Promise<{
14
+ readonly path: string;
15
+ readonly sha256: string;
16
+ readonly size: number;
17
+ }>;