@octanejs/cli 0.0.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/package.json +50 -0
  4. package/src/bin/octane.js +4 -0
  5. package/src/commands/add.js +138 -0
  6. package/src/commands/analyze.js +271 -0
  7. package/src/commands/bindings.js +55 -0
  8. package/src/commands/doctor/check.js +43 -0
  9. package/src/commands/doctor/checks/bundler.js +104 -0
  10. package/src/commands/doctor/checks/config.js +184 -0
  11. package/src/commands/doctor/checks/dependencies.js +120 -0
  12. package/src/commands/doctor/checks/environment.js +38 -0
  13. package/src/commands/doctor/checks/source.js +108 -0
  14. package/src/commands/doctor/checks/typescript.js +183 -0
  15. package/src/commands/doctor/index.js +118 -0
  16. package/src/commands/doctor/registry.js +32 -0
  17. package/src/commands/doctor/report.js +158 -0
  18. package/src/commands/explain.js +95 -0
  19. package/src/commands/info.js +54 -0
  20. package/src/commands/init/index.js +277 -0
  21. package/src/commands/init/templates.js +124 -0
  22. package/src/commands/mcp/add.js +241 -0
  23. package/src/commands/mcp/clients.js +281 -0
  24. package/src/commands/mcp/detect.js +58 -0
  25. package/src/commands/mcp/index.js +23 -0
  26. package/src/commands/mcp/remove.js +105 -0
  27. package/src/commands/mcp/server.js +46 -0
  28. package/src/commands/mcp/status.js +48 -0
  29. package/src/data/index.js +74 -0
  30. package/src/data/octane-data.json +953 -0
  31. package/src/index.js +4 -0
  32. package/src/kernel/args.js +181 -0
  33. package/src/kernel/banner.js +98 -0
  34. package/src/kernel/command.js +84 -0
  35. package/src/kernel/context.js +78 -0
  36. package/src/kernel/edit.js +238 -0
  37. package/src/kernel/errors.js +42 -0
  38. package/src/kernel/exec.js +62 -0
  39. package/src/kernel/help.js +97 -0
  40. package/src/kernel/install.js +43 -0
  41. package/src/kernel/jsonc.js +91 -0
  42. package/src/kernel/main.js +166 -0
  43. package/src/kernel/project.js +376 -0
  44. package/src/kernel/registry.js +52 -0
  45. package/src/kernel/semver.js +111 -0
  46. package/src/kernel/ui.js +155 -0
@@ -0,0 +1,183 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fail, pass, skip, warn } from '../check.js';
4
+ import { findNestedProperty, setCompilerOption } from '../../../kernel/edit.js';
5
+
6
+ const TSRX_PLUGIN = '@tsrx/typescript-plugin';
7
+ const WILDCARD_DECL = /^\s*declare\s+module\s+['"]\*\.tsrx['"]/;
8
+ const LEGACY_RESOLUTION = new Set(['node', 'node10', 'classic']);
9
+ // `tsc` as its own command word. `\btsc\b` also matches the tail of `vue-tsc`
10
+ // and `svelte-tsc`, which would fail a healthy script and, under --fix, rewrite
11
+ // it to `vue-tsrx-tsc`. Quotes count as delimiters because the --fix path
12
+ // splices the raw JSON value, quotes included.
13
+ const BARE_TSC = /(^|[\s;&|("'])tsc(?=[\s;&|)"']|$)/;
14
+ const BARE_TSC_GLOBAL = new RegExp(BARE_TSC.source, 'g');
15
+
16
+ /**
17
+ * @param {import('../../../kernel/project.js').Project} project
18
+ * @returns {boolean}
19
+ */
20
+ const hasTsconfig = (project) => project.tsconfig !== null;
21
+
22
+ /**
23
+ * @param {import('../../../kernel/project.js').Project} project
24
+ * @param {string} key
25
+ * @param {unknown} value
26
+ * @returns {import('../check.js').FixOutcome}
27
+ */
28
+ function applyCompilerOption(project, key, value) {
29
+ const file = /** @type {NonNullable<typeof project.tsconfig>} */ (project.tsconfig).path;
30
+ const source = readFileSync(file, 'utf8');
31
+ const edited = setCompilerOption(source, key, value);
32
+
33
+ if (!edited) {
34
+ return {
35
+ changed: false,
36
+ message: `Could not edit ${path.basename(file)} safely; set "${key}" by hand.`,
37
+ };
38
+ }
39
+ if (!edited.changed) return { changed: false, message: `"${key}" is already set.` };
40
+
41
+ writeFileSync(file, edited.text);
42
+ return {
43
+ changed: true,
44
+ message: `Set "${key}": ${JSON.stringify(value)}`,
45
+ files: [path.relative(project.root, file)],
46
+ };
47
+ }
48
+
49
+ /** @type {import('../check.js').Check[]} */
50
+ export const typescriptChecks = [
51
+ {
52
+ id: 'ts.jsx-import-source',
53
+ title: 'JSX import source',
54
+ category: 'typescript',
55
+ severity: 'error',
56
+ applies: hasTsconfig,
57
+ run(ctx, project) {
58
+ const options = project.tsconfig?.config.compilerOptions ?? {};
59
+ if (options.jsxImportSource === 'octane') return pass('jsxImportSource is "octane"');
60
+ return fail(
61
+ options.jsxImportSource
62
+ ? `jsxImportSource is "${options.jsxImportSource}", not "octane"`
63
+ : 'jsxImportSource is not set',
64
+ { detail: ["Octane JSX resolves through its own runtime, not React's."] },
65
+ );
66
+ },
67
+ fix: (ctx, project) => applyCompilerOption(project, 'jsxImportSource', 'octane'),
68
+ },
69
+ {
70
+ id: 'ts.tsrx-plugin',
71
+ title: 'TSRX language service',
72
+ category: 'typescript',
73
+ severity: 'error',
74
+ applies: (project) => hasTsconfig(project) && project.tsrxFiles.length > 0,
75
+ run(ctx, project) {
76
+ const plugins = project.tsconfig?.config.compilerOptions?.plugins;
77
+ if (Array.isArray(plugins) && plugins.some((plugin) => plugin?.name === TSRX_PLUGIN)) {
78
+ return pass(`${TSRX_PLUGIN} is registered`);
79
+ }
80
+ return fail(`${TSRX_PLUGIN} is not in compilerOptions.plugins`, {
81
+ detail: [
82
+ 'Without it the editor cannot resolve .tsrx modules, so every import is `any`.',
83
+ `Add: "plugins": [{ "name": "${TSRX_PLUGIN}" }]`,
84
+ ],
85
+ });
86
+ },
87
+ fix(ctx, project) {
88
+ const plugins = project.tsconfig?.config.compilerOptions?.plugins;
89
+ if (Array.isArray(plugins) && plugins.length > 0) {
90
+ return {
91
+ changed: false,
92
+ message: `compilerOptions.plugins already has entries; add { "name": "${TSRX_PLUGIN}" } by hand.`,
93
+ };
94
+ }
95
+ return applyCompilerOption(project, 'plugins', [{ name: TSRX_PLUGIN }]);
96
+ },
97
+ },
98
+ {
99
+ id: 'ts.module-resolution',
100
+ title: 'Module resolution',
101
+ category: 'typescript',
102
+ severity: 'warning',
103
+ applies: hasTsconfig,
104
+ run(ctx, project) {
105
+ const resolution = project.tsconfig?.config.compilerOptions?.moduleResolution;
106
+ if (!resolution) return warn('moduleResolution is not set; "bundler" is expected');
107
+ if (LEGACY_RESOLUTION.has(String(resolution).toLowerCase())) {
108
+ return warn(`moduleResolution is "${resolution}"`, {
109
+ detail: ['Octane packages publish export maps; use "bundler", "node16", or "nodenext".'],
110
+ });
111
+ }
112
+ return pass(`moduleResolution is "${resolution}"`);
113
+ },
114
+ },
115
+ {
116
+ id: 'ts.typecheck-script',
117
+ title: 'Typecheck script',
118
+ category: 'typescript',
119
+ severity: 'warning',
120
+ applies: (project) => project.tsrxFiles.length > 0,
121
+ run(ctx, project) {
122
+ const script = project.manifest.scripts?.typecheck;
123
+ if (!script) return skip('No typecheck script');
124
+ if (script.includes('tsrx-tsc')) return pass('typecheck runs tsrx-tsc');
125
+ if (!BARE_TSC.test(script)) return skip(`typecheck runs "${script}"`);
126
+ return fail('typecheck runs plain tsc', {
127
+ detail: [
128
+ 'Plain tsc cannot read .tsrx. Any program containing them must use tsrx-tsc.',
129
+ `Current: ${script}`,
130
+ ],
131
+ });
132
+ },
133
+ fix(ctx, project) {
134
+ const file = path.join(project.root, 'package.json');
135
+ const source = readFileSync(file, 'utf8');
136
+ const range = findNestedProperty(source, ['scripts', 'typecheck']);
137
+ if (!range) return { changed: false, message: 'Could not locate the typecheck script.' };
138
+
139
+ const current = source.slice(range.valueStart, range.valueEnd);
140
+ const replaced = current.replace(BARE_TSC_GLOBAL, (_m, before) => `${before}tsrx-tsc`);
141
+ if (replaced === current) return { changed: false, message: 'Nothing to replace.' };
142
+
143
+ writeFileSync(
144
+ file,
145
+ source.slice(0, range.valueStart) + replaced + source.slice(range.valueEnd),
146
+ );
147
+ return {
148
+ changed: true,
149
+ message: 'Rewrote typecheck to use tsrx-tsc',
150
+ files: ['package.json'],
151
+ };
152
+ },
153
+ },
154
+ {
155
+ id: 'ts.wildcard-tsrx-decl',
156
+ title: 'No wildcard .tsrx declaration',
157
+ category: 'typescript',
158
+ severity: 'error',
159
+ run(ctx, project) {
160
+ const offenders = project.sourceFiles.filter((file) => {
161
+ if (!/\.(?:[cm]?tsx?|d\.ts)$/.test(file)) return false;
162
+ try {
163
+ return readFileSync(file, 'utf8')
164
+ .split('\n')
165
+ .some((line) => WILDCARD_DECL.test(line));
166
+ } catch {
167
+ return false;
168
+ }
169
+ });
170
+
171
+ if (offenders.length === 0) return pass('No wildcard .tsrx module declaration');
172
+
173
+ return fail(`declare module '*.tsrx' found in ${offenders.length} file(s)`, {
174
+ detail: [
175
+ 'This silences .tsrx resolution instead of fixing it, so every import it covers',
176
+ 'resolves to `any`, including your own components.',
177
+ 'Remove it, then install @tsrx/typescript-plugin and typecheck with tsrx-tsc.',
178
+ ...offenders.map((file) => ` ${path.relative(project.root, file)}`),
179
+ ],
180
+ });
181
+ },
182
+ },
183
+ ];
@@ -0,0 +1,118 @@
1
+ import { defineCommand } from '../../kernel/command.js';
2
+ import { EXIT, usageError } from '../../kernel/errors.js';
3
+ import { summarizeProject } from '../../kernel/project.js';
4
+ import { fail, skip } from './check.js';
5
+ import { CATEGORIES, CHECKS } from './registry.js';
6
+ import { hasErrors, isRepairable, renderFindings, renderSummary, toJson } from './report.js';
7
+
8
+ /**
9
+ * @param {import('../../kernel/context.js').Ctx} ctx
10
+ * @param {import('../../kernel/project.js').Project} project
11
+ * @param {import('./check.js').Check} check
12
+ * @returns {Promise<import('./check.js').CheckResult>}
13
+ */
14
+ async function runCheck(ctx, project, check) {
15
+ if (check.applies && !check.applies(project)) return skip('Does not apply to this project');
16
+ try {
17
+ return await check.run(ctx, project);
18
+ } catch (error) {
19
+ // One broken check must not hide the other twenty.
20
+ return fail(`Check failed to run: ${error instanceof Error ? error.message : String(error)}`);
21
+ }
22
+ }
23
+
24
+ export default defineCommand({
25
+ description:
26
+ 'Inspect the project for the configuration and dependency mistakes that break Octane\n' +
27
+ 'quietly: a duplicated runtime, a missing JSX source, plain tsc over .tsrx.',
28
+ flags: {
29
+ fix: { type: 'boolean', description: 'Apply the repairs that are mechanical and unambiguous.' },
30
+ only: {
31
+ type: 'string',
32
+ repeatable: true,
33
+ placeholder: '<id>',
34
+ description: 'Run a single check by id. Repeatable.',
35
+ },
36
+ category: {
37
+ type: 'string',
38
+ placeholder: '<name>',
39
+ choices: CATEGORIES,
40
+ description: 'Run only one category of checks.',
41
+ },
42
+ },
43
+
44
+ async run(ctx, input) {
45
+ const selected = CHECKS.filter((check) => {
46
+ if (input.flags.category && check.category !== input.flags.category) return false;
47
+ if (input.flags.only?.length > 0 && !input.flags.only.includes(check.id)) return false;
48
+ return true;
49
+ });
50
+
51
+ if (selected.length === 0) {
52
+ throw usageError('No checks matched.', 'Run `octane doctor --help` to see the filters.');
53
+ }
54
+
55
+ const project = ctx.project();
56
+ ctx.ui.intro('octane doctor');
57
+
58
+ const spinner = ctx.ui.spinner(`Checking ${project.root}`);
59
+ /** @type {import('./report.js').Finding[]} */
60
+ const findings = [];
61
+ for (const check of selected) {
62
+ findings.push({ check, result: await runCheck(ctx, project, check) });
63
+ }
64
+ spinner.stop(`Ran ${selected.length} check(s)`);
65
+
66
+ if (input.flags.fix) await applyFixes(ctx, project, findings);
67
+
68
+ renderFindings(ctx, findings);
69
+ renderSummary(ctx, findings);
70
+
71
+ return {
72
+ exitCode: hasErrors(findings) ? EXIT.DIAGNOSTIC : EXIT.OK,
73
+ json: toJson(findings, summarizeProject(project)),
74
+ };
75
+ },
76
+ });
77
+
78
+ /**
79
+ * Repair what can be repaired, then re-run those checks so the report shows the
80
+ * state after the edit rather than the state that prompted it.
81
+ *
82
+ * @param {import('../../kernel/context.js').Ctx} ctx
83
+ * @param {import('../../kernel/project.js').Project} project
84
+ * @param {import('./report.js').Finding[]} findings
85
+ */
86
+ async function applyFixes(ctx, project, findings) {
87
+ const repairable = findings.filter(isRepairable);
88
+ if (repairable.length === 0) return;
89
+
90
+ if (ctx.dryRun) {
91
+ for (const finding of repairable) {
92
+ finding.fixed = { changed: false, message: 'would be fixed (--dry-run)' };
93
+ }
94
+ return;
95
+ }
96
+
97
+ const confirmed = await ctx.ui.confirm({
98
+ message: `Apply ${repairable.length} fix(es) to this project?`,
99
+ flag: '--yes',
100
+ initial: true,
101
+ });
102
+ if (!confirmed) return;
103
+
104
+ for (const finding of repairable) {
105
+ const fix = /** @type {NonNullable<typeof finding.check.fix>} */ (finding.check.fix);
106
+ try {
107
+ finding.fixed = await fix(ctx, project);
108
+ } catch (error) {
109
+ finding.fixed = {
110
+ changed: false,
111
+ message: `fix failed: ${error instanceof Error ? error.message : String(error)}`,
112
+ };
113
+ }
114
+ if (finding.fixed.changed) {
115
+ finding.result = await runCheck(ctx, ctx.refreshProject(), finding.check);
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,32 @@
1
+ import { bundlerChecks } from './checks/bundler.js';
2
+ import { configChecks } from './checks/config.js';
3
+ import { dependencyChecks } from './checks/dependencies.js';
4
+ import { environmentChecks } from './checks/environment.js';
5
+ import { sourceChecks } from './checks/source.js';
6
+ import { typescriptChecks } from './checks/typescript.js';
7
+
8
+ /**
9
+ * Report order, cheapest and most fundamental first: a wrong Node version or a
10
+ * duplicated runtime explains failures further down the list, so it is shown
11
+ * before them.
12
+ *
13
+ * @type {import('./check.js').Check['category'][]}
14
+ */
15
+ export const CATEGORIES = [
16
+ 'environment',
17
+ 'dependencies',
18
+ 'bundler',
19
+ 'typescript',
20
+ 'config',
21
+ 'source',
22
+ ];
23
+
24
+ /** @type {import('./check.js').Check[]} */
25
+ export const CHECKS = [
26
+ ...environmentChecks,
27
+ ...dependencyChecks,
28
+ ...bundlerChecks,
29
+ ...typescriptChecks,
30
+ ...configChecks,
31
+ ...sourceChecks,
32
+ ];
@@ -0,0 +1,158 @@
1
+ import { SYMBOLS } from '../../kernel/ui.js';
2
+ import { CATEGORIES } from './registry.js';
3
+
4
+ /**
5
+ * @typedef {Object} Finding
6
+ * @property {import('./check.js').Check} check
7
+ * @property {import('./check.js').CheckResult} result
8
+ * @property {import('./check.js').FixOutcome} [fixed]
9
+ */
10
+
11
+ /**
12
+ * A check reports what it found; its severity decides how much that counts.
13
+ * Everything downstream (glyph, summary, exit code) reads this one function, so
14
+ * the report can never show a red mark next to a count that calls it a warning.
15
+ *
16
+ * @param {Finding} finding
17
+ * @returns {'pass' | 'fail' | 'warn' | 'skip'}
18
+ */
19
+ export function level({ check, result }) {
20
+ if (result.status === 'pass' || result.status === 'skip') return result.status;
21
+ return result.status === 'fail' && check.severity === 'error' ? 'fail' : 'warn';
22
+ }
23
+
24
+ /**
25
+ * @param {Finding[]} findings
26
+ * @returns {{ pass: number, fail: number, warn: number, skip: number }}
27
+ */
28
+ export function summarize(findings) {
29
+ const totals = { pass: 0, fail: 0, warn: 0, skip: 0 };
30
+ for (const finding of findings) totals[level(finding)]++;
31
+ return totals;
32
+ }
33
+
34
+ /**
35
+ * Only an `error`-severity finding makes the run itself unsuccessful. Warnings
36
+ * are advice, and a `skip` means the check did not apply.
37
+ *
38
+ * @param {Finding[]} findings
39
+ * @returns {boolean}
40
+ */
41
+ export function hasErrors(findings) {
42
+ return findings.some((finding) => level(finding) === 'fail');
43
+ }
44
+
45
+ /**
46
+ * A finding is worth repairing when it reported a problem and its check knows
47
+ * how to fix it.
48
+ *
49
+ * @param {Finding} finding
50
+ * @returns {boolean}
51
+ */
52
+ export function isRepairable(finding) {
53
+ return (
54
+ (finding.result.status === 'fail' || finding.result.status === 'warn') &&
55
+ Boolean(finding.check.fix)
56
+ );
57
+ }
58
+
59
+ /**
60
+ * @param {import('../../kernel/ui.js').Ui} ui
61
+ * @param {Finding} finding
62
+ * @returns {string}
63
+ */
64
+ function glyph(ui, finding) {
65
+ const { colors } = ui;
66
+ switch (level(finding)) {
67
+ case 'pass':
68
+ return colors.green(SYMBOLS.pass);
69
+ case 'skip':
70
+ return colors.dim(SYMBOLS.skip);
71
+ case 'warn':
72
+ return colors.yellow(SYMBOLS.warn);
73
+ default:
74
+ return colors.red(SYMBOLS.fail);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * @param {import('../../kernel/context.js').Ctx} ctx
80
+ * @param {Finding[]} findings
81
+ */
82
+ export function renderFindings(ctx, findings) {
83
+ const { ui } = ctx;
84
+ const visible = ctx.verbose ? findings : findings.filter((f) => f.result.status !== 'skip');
85
+ const width = visible.reduce((max, f) => Math.max(max, f.check.title.length), 0);
86
+
87
+ for (const category of CATEGORIES) {
88
+ const group = visible.filter((f) => f.check.category === category);
89
+ if (group.length === 0) continue;
90
+
91
+ ui.log('');
92
+ ui.log(ui.colors.bold(category));
93
+
94
+ for (const finding of group) {
95
+ const { check, result, fixed } = finding;
96
+ const title = check.title.padEnd(width);
97
+ ui.log(` ${glyph(ui, finding)} ${title} ${ui.colors.dim(result.message)}`);
98
+
99
+ if (result.status !== 'pass' && result.status !== 'skip') {
100
+ for (const line of result.detail ?? []) ui.log(` ${ui.colors.dim(line)}`);
101
+ }
102
+ if (fixed?.changed) ui.log(` ${ui.colors.green(`fixed: ${fixed.message}`)}`);
103
+ else if (fixed) ui.log(` ${ui.colors.yellow(fixed.message)}`);
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * @param {import('../../kernel/context.js').Ctx} ctx
110
+ * @param {Finding[]} findings
111
+ */
112
+ export function renderSummary(ctx, findings) {
113
+ const { colors } = ctx.ui;
114
+ const totals = summarize(findings);
115
+ /** @type {string[]} */
116
+ const parts = [];
117
+
118
+ if (totals.fail > 0) parts.push(colors.red(`${totals.fail} failing`));
119
+ if (totals.warn > 0)
120
+ parts.push(colors.yellow(`${totals.warn} warning${totals.warn === 1 ? '' : 's'}`));
121
+ parts.push(colors.green(`${totals.pass} passing`));
122
+ if (totals.skip > 0 && ctx.verbose) parts.push(colors.dim(`${totals.skip} skipped`));
123
+
124
+ ctx.ui.log('');
125
+ ctx.ui.log(parts.join(colors.dim(' · ')));
126
+
127
+ const fixable = findings.filter((finding) => isRepairable(finding) && !finding.fixed);
128
+ if (fixable.length > 0) {
129
+ const noun = fixable.length === 1 ? 'finding' : 'findings';
130
+ ctx.ui.log(colors.dim(`\`octane doctor --fix\` can repair ${fixable.length} ${noun}.`));
131
+ }
132
+ }
133
+
134
+ /**
135
+ * @param {Finding[]} findings
136
+ * @param {Record<string, unknown>} project
137
+ * @returns {Record<string, unknown>}
138
+ */
139
+ export function toJson(findings, project) {
140
+ return {
141
+ ok: !hasErrors(findings),
142
+ summary: summarize(findings),
143
+ project,
144
+ checks: findings.map(({ check, result, fixed }) => ({
145
+ id: check.id,
146
+ title: check.title,
147
+ category: check.category,
148
+ severity: check.severity,
149
+ status: result.status,
150
+ message: result.message,
151
+ detail: result.detail ?? [],
152
+ fixable: Boolean(check.fix),
153
+ ...(fixed
154
+ ? { fixed: { changed: fixed.changed, message: fixed.message, files: fixed.files ?? [] } }
155
+ : {}),
156
+ })),
157
+ };
158
+ }
@@ -0,0 +1,95 @@
1
+ import { defineCommand } from '../kernel/command.js';
2
+ import { DATA, ERROR_DOCS_URL } from '../data/index.js';
3
+ import { CliError, usageError } from '../kernel/errors.js';
4
+
5
+ /**
6
+ * Pull the code and any encoded arguments out of whatever the user pasted.
7
+ *
8
+ * A production build reports `Minified Octane error #3; visit
9
+ * https://octanejs.dev/errors/3?args[]=...`, which hides the real message and
10
+ * its arguments behind a URL. Accepting that string verbatim is the whole point
11
+ * of this command, so a bare number, a `#3`, and the full sentence all work.
12
+ *
13
+ * @param {string} input
14
+ * @returns {{ code: string, args: string[] } | null}
15
+ */
16
+ export function parseErrorReference(input) {
17
+ const code = /(?:#|errors\/)(\d+)|^\s*(\d+)\s*$/.exec(input);
18
+ if (!code) return null;
19
+
20
+ const args = [...input.matchAll(/args\[\]=([^&\s]*)/g)].map(([, value]) => {
21
+ try {
22
+ return decodeURIComponent(value);
23
+ } catch {
24
+ return value;
25
+ }
26
+ });
27
+
28
+ return { code: code[1] ?? code[2], args };
29
+ }
30
+
31
+ /**
32
+ * @param {string} template
33
+ * @param {string[]} args
34
+ * @returns {string}
35
+ */
36
+ function fill(template, args) {
37
+ let index = 0;
38
+ return template.replace(/%s/g, () => (index < args.length ? args[index++] : '%s'));
39
+ }
40
+
41
+ export default defineCommand({
42
+ description:
43
+ 'Look up an Octane runtime error. Accepts a bare code, or the whole minified\n' +
44
+ 'message a production build prints, arguments included.',
45
+ positionals: [
46
+ { name: 'error', description: 'An error code, URL, or pasted message.', required: true },
47
+ ],
48
+
49
+ async run(ctx, input) {
50
+ const raw = input.positionals.join(' ').trim();
51
+ if (!raw) throw usageError('Nothing to explain.', 'Try: octane explain 3');
52
+
53
+ const reference = parseErrorReference(raw);
54
+ if (!reference) {
55
+ throw usageError(`Could not find an error code in "${raw}".`, 'Try: octane explain 3');
56
+ }
57
+
58
+ const entry = DATA.errorCodes[reference.code];
59
+ if (!entry) {
60
+ throw new CliError(`Unknown Octane error code ${reference.code}.`, {
61
+ hint: `This CLI knows codes 1 to ${Object.keys(DATA.errorCodes).length}. Upgrade @octanejs/cli if the code is newer.`,
62
+ });
63
+ }
64
+
65
+ const message = fill(entry.message, reference.args);
66
+ const url = `${ERROR_DOCS_URL}${reference.code}`;
67
+
68
+ ctx.ui.intro(`Octane error #${reference.code}`);
69
+ ctx.ui.log('');
70
+ ctx.ui.log(` ${message}`);
71
+ ctx.ui.log('');
72
+ ctx.ui.log(` ${ctx.ui.colors.dim(`runtime: ${entry.runtime.join(', ')}`)}`);
73
+ if (entry.status !== 'active') {
74
+ ctx.ui.log(` ${ctx.ui.colors.yellow(`status: ${entry.status}`)}`);
75
+ }
76
+ if (entry.message.includes('%s') && reference.args.length === 0) {
77
+ ctx.ui.log(
78
+ ` ${ctx.ui.colors.dim('Paste the whole error message to have its arguments filled in.')}`,
79
+ );
80
+ }
81
+ ctx.ui.outro(url);
82
+
83
+ return {
84
+ json: {
85
+ code: Number(reference.code),
86
+ message,
87
+ template: entry.message,
88
+ args: reference.args,
89
+ runtime: entry.runtime,
90
+ status: entry.status,
91
+ url,
92
+ },
93
+ };
94
+ },
95
+ });
@@ -0,0 +1,54 @@
1
+ import { defineCommand } from '../kernel/command.js';
2
+ import { summarizeProject } from '../kernel/project.js';
3
+
4
+ /**
5
+ * @param {unknown} value
6
+ * @returns {string}
7
+ */
8
+ function show(value) {
9
+ if (value === null || value === undefined) return '-';
10
+ if (Array.isArray(value)) return value.length === 0 ? '-' : String(value.length);
11
+ return String(value);
12
+ }
13
+
14
+ export default defineCommand({
15
+ description: 'Print the environment and project details worth pasting into a bug report.',
16
+
17
+ async run(ctx) {
18
+ const project = ctx.project();
19
+ const summary = summarizeProject(project);
20
+
21
+ const environment = {
22
+ cli: ctx.version,
23
+ node: process.versions.node,
24
+ platform: `${process.platform} ${process.arch}`,
25
+ };
26
+
27
+ ctx.ui.note('Environment', [
28
+ `CLI ${environment.cli}`,
29
+ `Node ${environment.node}`,
30
+ `Platform ${environment.platform}`,
31
+ ]);
32
+
33
+ ctx.ui.note('Project', [
34
+ `Name ${show(summary.name)}`,
35
+ `Root ${show(summary.root)}`,
36
+ `Manager ${show(summary.packageManager)}`,
37
+ `Bundler ${show(summary.bundler)}${summary.bundlerConfig ? ` (${summary.bundlerConfig})` : ''}`,
38
+ `Octane ${show(summary.octane)}`,
39
+ `Config ${show(summary.octaneConfig)}`,
40
+ `tsconfig ${show(summary.tsconfig)}`,
41
+ `.tsrx ${show(summary.tsrxFileCount)} file(s)`,
42
+ ]);
43
+
44
+ const bindings = /** @type {{ name: string, version: string | null }[]} */ (summary.bindings);
45
+ if (bindings.length > 0) {
46
+ ctx.ui.note(
47
+ 'Bindings',
48
+ bindings.map((binding) => `${binding.name}@${binding.version ?? 'not installed'}`),
49
+ );
50
+ }
51
+
52
+ return { json: { environment, project: summary } };
53
+ },
54
+ });