@paperboyseo/cli 0.0.0-stage → 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/main.js ADDED
@@ -0,0 +1,77 @@
1
+ import { createInterface } from 'node:readline/promises';
2
+ import { parse } from './args.js';
3
+ import { makeClient } from './context.js';
4
+ import { CliError } from './errors.js';
5
+ import { readConfig, resolveSettings } from './config.js';
6
+ import { openBrowser } from './open.js';
7
+ import { Output, colorEnabled } from './output.js';
8
+ import { VERSION } from './version.js';
9
+ import { check } from './commands/check.js';
10
+ import { connect } from './commands/connect.js';
11
+ import { mcp } from './commands/mcp.js';
12
+ import { publish } from './commands/publish.js';
13
+ import { report } from './commands/report.js';
14
+ import { status } from './commands/status.js';
15
+ import { topics } from './commands/topics.js';
16
+ import { write } from './commands/write.js';
17
+ import { HELP } from './help.js';
18
+ const COMMANDS = { connect, topics, write, check, publish, status, report, mcp };
19
+ async function defaultConfirm(question) {
20
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
21
+ try {
22
+ const answer = await rl.question(`${question} [y/N] `);
23
+ return /^y(es)?$/i.test(answer.trim());
24
+ }
25
+ finally {
26
+ rl.close();
27
+ }
28
+ }
29
+ /** Run the CLI and return the process exit code: 0 ok, 1 check failed, 2 error. */
30
+ export async function run(argv, opts = {}) {
31
+ const env = opts.env ?? process.env;
32
+ const stdout = opts.stdout ?? process.stdout;
33
+ const stderr = opts.stderr ?? process.stderr;
34
+ const out = new Output({
35
+ stdout,
36
+ stderr,
37
+ json: argv.includes('--json'),
38
+ color: colorEnabled({ env, stdout, noColorFlag: argv.includes('--no-color') }),
39
+ });
40
+ try {
41
+ const { flags, positionals } = parse(argv);
42
+ const [command, ...args] = positionals;
43
+ if (flags.version) {
44
+ out.result('version', { version: VERSION }, () => out.line(`paperboy ${VERSION}`));
45
+ return 0;
46
+ }
47
+ if (flags.help || !command || command === 'help') {
48
+ out.result('help', { usage: HELP }, () => out.line(HELP));
49
+ return 0;
50
+ }
51
+ const handler = COMMANDS[command];
52
+ if (!handler)
53
+ throw new CliError('usage', `Unknown command "${command}". Run \`paperboy --help\` for the list.`);
54
+ const settings = resolveSettings(flags, env, readConfig(env));
55
+ const interactive = opts.interactive ?? Boolean(stdout.isTTY && process.stdin.isTTY);
56
+ const ctx = {
57
+ flags,
58
+ args,
59
+ env,
60
+ out,
61
+ settings,
62
+ deps: {
63
+ fetch: opts.fetch,
64
+ sleep: opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
65
+ open: opts.open ?? openBrowser,
66
+ confirm: opts.confirm ?? defaultConfirm,
67
+ interactive,
68
+ },
69
+ client: (overrides) => makeClient(ctx, overrides),
70
+ };
71
+ return await handler(ctx);
72
+ }
73
+ catch (err) {
74
+ out.fail(err);
75
+ return 2;
76
+ }
77
+ }
package/dist/open.js ADDED
@@ -0,0 +1,29 @@
1
+ import { spawn } from 'node:child_process';
2
+ /** Best effort: open a URL in the default browser. Never throws. */
3
+ export const openBrowser = (url) => {
4
+ if (!/^https?:\/\//i.test(url))
5
+ return;
6
+ let cmd;
7
+ let args;
8
+ switch (process.platform) {
9
+ case 'darwin':
10
+ cmd = 'open';
11
+ args = [url];
12
+ break;
13
+ case 'win32':
14
+ cmd = 'cmd';
15
+ args = ['/c', 'start', '""', url.replace(/&/g, '^&')];
16
+ break;
17
+ default:
18
+ cmd = 'xdg-open';
19
+ args = [url];
20
+ }
21
+ try {
22
+ const child = spawn(cmd, args, { stdio: 'ignore', detached: true, windowsVerbatimArguments: process.platform === 'win32' });
23
+ child.on('error', () => { });
24
+ child.unref();
25
+ }
26
+ catch {
27
+ // The URL is always printed as well, so failing to open is fine.
28
+ }
29
+ };
package/dist/output.js ADDED
@@ -0,0 +1,99 @@
1
+ import { CliError } from './errors.js';
2
+ function wrap(on, open, close) {
3
+ return (s) => (on ? `\u001b[${open}m${s}\u001b[${close}m` : s);
4
+ }
5
+ export function makeStyle(on) {
6
+ return {
7
+ bold: wrap(on, 1, 22),
8
+ dim: wrap(on, 2, 22),
9
+ red: wrap(on, 31, 39),
10
+ green: wrap(on, 32, 39),
11
+ yellow: wrap(on, 33, 39),
12
+ cyan: wrap(on, 36, 39),
13
+ };
14
+ }
15
+ export function colorEnabled(opts) {
16
+ if (opts.noColorFlag)
17
+ return false;
18
+ if (opts.env['NO_COLOR'] !== undefined && opts.env['NO_COLOR'] !== '')
19
+ return false;
20
+ if (opts.env['FORCE_COLOR'] && opts.env['FORCE_COLOR'] !== '0')
21
+ return true;
22
+ return Boolean(opts.stdout.isTTY) && opts.env['TERM'] !== 'dumb';
23
+ }
24
+ export class Output {
25
+ json;
26
+ style;
27
+ stdout;
28
+ stderr;
29
+ constructor(opts) {
30
+ this.stdout = opts.stdout;
31
+ this.stderr = opts.stderr;
32
+ this.json = opts.json;
33
+ this.style = makeStyle(opts.color);
34
+ }
35
+ /** Human mode only: print a line to stdout. */
36
+ line(text = '') {
37
+ if (!this.json)
38
+ this.stdout.write(text + '\n');
39
+ }
40
+ /** Alias of line, used for progress notes. */
41
+ info(text) {
42
+ this.line(text);
43
+ }
44
+ /** Things the user must see even in JSON mode (device codes, links): stdout when human, stderr when JSON. */
45
+ prompt(text) {
46
+ if (this.json)
47
+ this.stderr.write(text + '\n');
48
+ else
49
+ this.stdout.write(text + '\n');
50
+ }
51
+ /** Emit the result of a command: one JSON object in JSON mode, otherwise run the human renderer. */
52
+ result(command, data, human) {
53
+ if (this.json)
54
+ this.stdout.write(JSON.stringify({ ok: true, command, data }) + '\n');
55
+ else
56
+ human();
57
+ }
58
+ fail(err) {
59
+ const e = toCliError(err);
60
+ if (this.json) {
61
+ const error = { code: e.code, message: e.message };
62
+ if (e.status !== undefined)
63
+ error.status = e.status;
64
+ this.stdout.write(JSON.stringify({ ok: false, error }) + '\n');
65
+ }
66
+ else {
67
+ this.stderr.write(`${this.style.red('error:')} ${e.message}\n`);
68
+ }
69
+ }
70
+ }
71
+ export function toCliError(err) {
72
+ if (err instanceof CliError)
73
+ return err;
74
+ const message = err instanceof Error ? err.message : String(err);
75
+ return new CliError('unexpected_error', message);
76
+ }
77
+ export function num(n) {
78
+ return typeof n === 'number' && Number.isFinite(n) ? n.toLocaleString('en-US') : '0';
79
+ }
80
+ export function fixed(n, digits = 1) {
81
+ return typeof n === 'number' && Number.isFinite(n) ? n.toFixed(digits) : '-';
82
+ }
83
+ const BARS = '▁▂▃▄▅▆▇█';
84
+ export function sparkline(values) {
85
+ if (values.length === 0)
86
+ return '';
87
+ const max = Math.max(...values);
88
+ const min = Math.min(...values);
89
+ const span = max - min;
90
+ return values
91
+ .map((v) => BARS[span === 0 ? (max === 0 ? 0 : 3) : Math.round(((v - min) / span) * (BARS.length - 1))])
92
+ .join('');
93
+ }
94
+ export function padRight(s, width) {
95
+ return s.length >= width ? s : s + ' '.repeat(width - s.length);
96
+ }
97
+ export function padLeft(s, width) {
98
+ return s.length >= width ? s : ' '.repeat(width - s.length) + s;
99
+ }
package/dist/site.js ADDED
@@ -0,0 +1,43 @@
1
+ import { CliError } from './errors.js';
2
+ function normalizeDomain(value) {
3
+ return value
4
+ .trim()
5
+ .toLowerCase()
6
+ .replace(/^https?:\/\//, '')
7
+ .replace(/^www\./, '')
8
+ .replace(/\/.*$/, '');
9
+ }
10
+ async function listSites(api) {
11
+ const res = await api.get('/api/v1/sites');
12
+ return Array.isArray(res.data.sites) ? res.data.sites : [];
13
+ }
14
+ function describe(sites) {
15
+ return sites.map((s) => ` ${s.domain} (${s.id})`).join('\n');
16
+ }
17
+ /**
18
+ * Resolve the site id to operate on: --site (id or domain), then the saved default,
19
+ * then the only site on the account.
20
+ */
21
+ export async function resolveSiteId(ctx) {
22
+ const api = ctx.client();
23
+ const wanted = ctx.settings.site;
24
+ if (wanted) {
25
+ // Domains always contain a dot and ids never do, so ids skip the lookup.
26
+ if (!wanted.includes('.'))
27
+ return wanted;
28
+ const sites = await listSites(api);
29
+ const target = normalizeDomain(wanted);
30
+ const match = sites.find((s) => s.id === wanted || normalizeDomain(s.domain) === target);
31
+ if (match)
32
+ return match.id;
33
+ throw new CliError('site_not_found', `No site matches "${wanted}".` + (sites.length ? `\nYour sites:\n${describe(sites)}` : ' Run `paperboy connect https://yoursite.com` to add one.'));
34
+ }
35
+ const sites = await listSites(api);
36
+ const [only] = sites;
37
+ if (sites.length === 1 && only)
38
+ return only.id;
39
+ if (sites.length === 0) {
40
+ throw new CliError('no_site', 'No sites on this account yet. Run `paperboy connect https://yoursite.com` to add one.');
41
+ }
42
+ throw new CliError('site_required', `More than one site on this account. Pick one with --site <id|domain>:\n${describe(sites)}`);
43
+ }
@@ -0,0 +1,2 @@
1
+ export const VERSION = '0.1.0';
2
+ export const DEFAULT_BASE_URL = 'https://paperboyseo.com';
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "@paperboyseo/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Command line interface for Paperboy, the open-source growth engine that reads Search Console, picks topics, writes and fact-checks articles, publishes and measures.",
5
+ "license": "AGPL-3.0-only",
6
+ "type": "module",
7
+ "bin": {
8
+ "paperboy": "dist/cli.js",
9
+ "paperboy-seo": "dist/cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/paperboyseo/paperboy.git",
19
+ "directory": "packages/cli"
20
+ },
21
+ "homepage": "https://paperboyseo.com/docs/cli",
22
+ "bugs": {
23
+ "url": "https://github.com/paperboyseo/paperboy/issues"
24
+ },
25
+ "keywords": [
26
+ "paperboy",
27
+ "seo",
28
+ "search-console",
29
+ "content",
30
+ "fact-check",
31
+ "cli",
32
+ "growth"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "provenance": true
37
+ },
38
+ "engines": {
39
+ "node": ">=22"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.json && node -e \"require('node:fs').chmodSync('dist/cli.js', 0o755)\"",
43
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
44
+ "test": "vitest run",
45
+ "prepublishOnly": "npm run build && npm test"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^22.10.0",
49
+ "typescript": "^5.9.0",
50
+ "vitest": "^3.2.0"
51
+ }
52
+ }