proteum 2.0.0 → 2.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/AGENTS.md +13 -1
- package/README.md +375 -0
- package/agents/framework/AGENTS.md +917 -0
- package/agents/project/AGENTS.md +138 -0
- package/agents/{codex → project}/CODING_STYLE.md +3 -2
- package/agents/project/client/AGENTS.md +108 -0
- package/agents/{codex → project}/client/pages/AGENTS.md +8 -8
- package/agents/{codex → project}/server/routes/AGENTS.md +2 -1
- package/agents/project/server/services/AGENTS.md +170 -0
- package/agents/{codex → project}/tests/AGENTS.md +1 -0
- package/cli/app/config.ts +3 -2
- package/cli/app/index.ts +6 -66
- package/cli/bin.js +7 -2
- package/cli/commands/build.ts +94 -27
- package/cli/commands/check.ts +15 -1
- package/cli/commands/dev.ts +288 -132
- package/cli/commands/doctor.ts +108 -0
- package/cli/commands/explain.ts +226 -0
- package/cli/commands/init.ts +76 -70
- package/cli/commands/lint.ts +18 -1
- package/cli/commands/refresh.ts +16 -6
- package/cli/commands/typecheck.ts +14 -1
- package/cli/compiler/artifacts/controllers.ts +150 -0
- package/cli/compiler/artifacts/discovery.ts +132 -0
- package/cli/compiler/artifacts/manifest.ts +267 -0
- package/cli/compiler/artifacts/routing.ts +315 -0
- package/cli/compiler/artifacts/services.ts +480 -0
- package/cli/compiler/artifacts/shared.ts +12 -0
- package/cli/compiler/client/identite.ts +2 -1
- package/cli/compiler/client/index.ts +13 -3
- package/cli/compiler/common/controllers.ts +23 -28
- package/cli/compiler/common/files/style.ts +3 -4
- package/cli/compiler/common/generatedRouteModules.ts +333 -19
- package/cli/compiler/common/proteumManifest.ts +133 -0
- package/cli/compiler/index.ts +33 -896
- package/cli/compiler/server/index.ts +21 -4
- package/cli/context.ts +71 -0
- package/cli/index.ts +39 -181
- package/cli/presentation/commands.ts +208 -0
- package/cli/presentation/compileReporter.ts +65 -0
- package/cli/presentation/devSession.ts +70 -0
- package/cli/presentation/help.ts +193 -0
- package/cli/presentation/ink.ts +69 -0
- package/cli/presentation/layout.ts +83 -0
- package/cli/runtime/argv.ts +49 -0
- package/cli/runtime/command.ts +25 -0
- package/cli/runtime/commands.ts +221 -0
- package/cli/runtime/importEsm.ts +7 -0
- package/cli/runtime/verbose.ts +15 -0
- package/cli/utils/agents.ts +5 -4
- package/cli/utils/keyboard.ts +12 -6
- package/client/app/index.ts +0 -6
- package/client/services/router/index.tsx +1 -1
- package/client/services/router/response/index.tsx +2 -2
- package/common/dev/serverHotReload.ts +12 -0
- package/common/router/index.ts +3 -2
- package/common/router/layouts.ts +1 -1
- package/common/router/pageSetup.ts +1 -0
- package/package.json +10 -8
- package/prettier/router-registration-plugin.cjs +52 -0
- package/prettier.config.cjs +1 -0
- package/scripts/cleanup-generated-controllers.ts +2 -2
- package/scripts/fix-reference-app-typing.ts +2 -2
- package/scripts/format-router-registrations.ts +119 -0
- package/scripts/migrate-explicit-controllers-and-request.ts +423 -0
- package/scripts/refactor-server-controllers.ts +19 -18
- package/scripts/refactor-server-runtime-aliases.ts +1 -1
- package/server/app/commands.ts +309 -25
- package/server/app/container/config.ts +1 -1
- package/server/app/container/index.ts +2 -2
- package/server/app/controller/index.ts +13 -4
- package/server/app/index.ts +53 -37
- package/server/app/service/container.ts +26 -28
- package/server/app/service/index.ts +10 -20
- package/server/app.tsconfig.json +9 -2
- package/server/index.ts +32 -1
- package/server/services/auth/index.ts +234 -15
- package/server/services/auth/router/index.ts +39 -7
- package/server/services/auth/router/request.ts +40 -8
- package/server/services/disks/index.ts +1 -1
- package/server/services/prisma/Facet.ts +2 -2
- package/server/services/prisma/index.ts +22 -5
- package/server/services/prisma/mariadb.ts +47 -0
- package/server/services/router/http/index.ts +9 -1
- package/server/services/router/index.ts +10 -4
- package/server/services/router/response/index.ts +26 -6
- package/types/auth-check-rules.test.ts +51 -0
- package/types/controller-request-context.test.ts +55 -0
- package/types/service-config.test.ts +39 -0
- package/agents/codex/AGENTS.md +0 -95
- package/agents/codex/client/AGENTS.md +0 -102
- package/agents/codex/server/services/AGENTS.md +0 -137
- package/server/services/models.7z +0 -0
- /package/agents/{codex → project}/agents.md.zip +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const React = require('react') as typeof import('react');
|
|
2
|
+
|
|
3
|
+
import { renderRows } from './layout';
|
|
4
|
+
import { renderInk } from './ink';
|
|
5
|
+
|
|
6
|
+
const ProteumWordmark = [
|
|
7
|
+
String.raw` ____ ____ ___ _____ _____ _ _ __ __`,
|
|
8
|
+
String.raw`| _ \| _ \ / _ \_ _| ____| | | | \/ |`,
|
|
9
|
+
String.raw`| |_) | |_) | | | || | | _| | | | | |\/| |`,
|
|
10
|
+
String.raw`| __/| _ <| |_| || | | |___| |_| | | | |`,
|
|
11
|
+
String.raw`|_| |_| \_\\___/ |_| |_____|\___/|_| |_|`,
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const renderDevSession = async ({
|
|
15
|
+
appName,
|
|
16
|
+
appRoot,
|
|
17
|
+
routerPort,
|
|
18
|
+
devEventPort,
|
|
19
|
+
}: {
|
|
20
|
+
appName: string;
|
|
21
|
+
appRoot: string;
|
|
22
|
+
routerPort: number;
|
|
23
|
+
devEventPort: number;
|
|
24
|
+
}) =>
|
|
25
|
+
[
|
|
26
|
+
await renderInk(({ Box, Text }) => {
|
|
27
|
+
const createElement = React.createElement;
|
|
28
|
+
const wordmark = ProteumWordmark.map((line) =>
|
|
29
|
+
createElement(Text, { key: line, bold: true, color: 'cyan' }, line),
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
return createElement(
|
|
33
|
+
Box,
|
|
34
|
+
{ borderStyle: 'round', borderColor: 'cyan', paddingX: 2, paddingY: 0, flexDirection: 'column' },
|
|
35
|
+
createElement(Text, { bold: true, color: 'green' }, 'PROTEUM DEV'),
|
|
36
|
+
createElement(Text, { dimColor: true }, 'Agent-first SSR compiler and server loop.'),
|
|
37
|
+
createElement(Box, { flexDirection: 'column', marginTop: 1 }, ...wordmark),
|
|
38
|
+
);
|
|
39
|
+
}),
|
|
40
|
+
renderRows(
|
|
41
|
+
[
|
|
42
|
+
{ label: 'app', value: appName },
|
|
43
|
+
{ label: 'root', value: appRoot },
|
|
44
|
+
{ label: 'router', value: `http://localhost:${routerPort}` },
|
|
45
|
+
{ label: 'hmr', value: `http://localhost:${devEventPort}/__proteum_hmr` },
|
|
46
|
+
{ label: 'hotkeys', value: 'Ctrl+R reload, Ctrl+C stop' },
|
|
47
|
+
],
|
|
48
|
+
{ minLabelWidth: 12, maxLabelWidth: 12 },
|
|
49
|
+
),
|
|
50
|
+
].join('\n\n');
|
|
51
|
+
|
|
52
|
+
export const renderServerReadyBanner = async ({
|
|
53
|
+
appName,
|
|
54
|
+
publicUrl,
|
|
55
|
+
}: {
|
|
56
|
+
appName: string;
|
|
57
|
+
publicUrl: string;
|
|
58
|
+
}) =>
|
|
59
|
+
renderInk(({ Box, Text }) => {
|
|
60
|
+
const createElement = React.createElement;
|
|
61
|
+
|
|
62
|
+
return createElement(
|
|
63
|
+
Box,
|
|
64
|
+
{ borderStyle: 'round', borderColor: 'green', paddingX: 2, paddingY: 0, flexDirection: 'column' },
|
|
65
|
+
createElement(Text, { bold: true, backgroundColor: 'green', color: 'black' }, ' SERVER READY '),
|
|
66
|
+
createElement(Text, { bold: true, color: 'green' }, appName),
|
|
67
|
+
createElement(Text, { bold: true }, publicUrl),
|
|
68
|
+
createElement(Text, { dimColor: true }, 'SSR server is listening for requests and hot reloads.'),
|
|
69
|
+
);
|
|
70
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { renderRows, wrapText } from './layout';
|
|
2
|
+
import {
|
|
3
|
+
getInitAvailabilityNote,
|
|
4
|
+
isLikelyProteumAppRoot,
|
|
5
|
+
proteumCommandGroups,
|
|
6
|
+
proteumCommandNames,
|
|
7
|
+
proteumCommands,
|
|
8
|
+
proteumRecommendedFlow,
|
|
9
|
+
type TProteumCommandName,
|
|
10
|
+
} from './commands';
|
|
11
|
+
import { renderSection, renderTitle } from './ink';
|
|
12
|
+
|
|
13
|
+
type TCommandDefinition = {
|
|
14
|
+
options: Array<{
|
|
15
|
+
preferredName: string;
|
|
16
|
+
definition: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
required: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
} | null;
|
|
21
|
+
|
|
22
|
+
type THelpRequest =
|
|
23
|
+
| { kind: 'none' }
|
|
24
|
+
| { kind: 'overview' }
|
|
25
|
+
| { kind: 'command'; commandName: TProteumCommandName };
|
|
26
|
+
|
|
27
|
+
const commandNameSet = new Set<TProteumCommandName>(proteumCommandNames);
|
|
28
|
+
|
|
29
|
+
const renderExamples = (examples: Array<{ description: string; command: string }>) =>
|
|
30
|
+
examples
|
|
31
|
+
.map((example) =>
|
|
32
|
+
[
|
|
33
|
+
` ${example.command}`,
|
|
34
|
+
wrapText(example.description, { indent: ' ', nextIndent: ' ' }),
|
|
35
|
+
].join('\n'),
|
|
36
|
+
)
|
|
37
|
+
.join('\n');
|
|
38
|
+
|
|
39
|
+
const renderNotes = (notes: string[]) =>
|
|
40
|
+
notes.map((note) => wrapText(note, { indent: ' - ', nextIndent: ' ' })).join('\n');
|
|
41
|
+
|
|
42
|
+
const renderOptions = (definition: TCommandDefinition) => {
|
|
43
|
+
if (!definition || definition.options.length === 0) return ' This command has no options.';
|
|
44
|
+
|
|
45
|
+
return renderRows(
|
|
46
|
+
definition.options.map((option) => ({
|
|
47
|
+
label: option.definition,
|
|
48
|
+
value: option.description || 'No description.',
|
|
49
|
+
})),
|
|
50
|
+
{ minLabelWidth: 18, maxLabelWidth: 34 },
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const resolveCustomHelpRequest = (argv: string[]): THelpRequest => {
|
|
55
|
+
if (argv.length === 0) return { kind: 'overview' };
|
|
56
|
+
if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h' || argv[0] === 'help')) return { kind: 'overview' };
|
|
57
|
+
|
|
58
|
+
if (argv[0] === 'help' && commandNameSet.has(argv[1] as TProteumCommandName))
|
|
59
|
+
return { kind: 'command', commandName: argv[1] as TProteumCommandName };
|
|
60
|
+
|
|
61
|
+
if (commandNameSet.has(argv[0] as TProteumCommandName) && argv.some((arg) => arg === '--help' || arg === '-h'))
|
|
62
|
+
return { kind: 'command', commandName: argv[0] as TProteumCommandName };
|
|
63
|
+
|
|
64
|
+
return { kind: 'none' };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const renderCliOverview = async ({
|
|
68
|
+
version,
|
|
69
|
+
workdir,
|
|
70
|
+
initAvailable,
|
|
71
|
+
}: {
|
|
72
|
+
version: string;
|
|
73
|
+
workdir: string;
|
|
74
|
+
initAvailable: boolean;
|
|
75
|
+
}) => {
|
|
76
|
+
const sections: string[] = [];
|
|
77
|
+
|
|
78
|
+
sections.push(
|
|
79
|
+
await renderTitle(
|
|
80
|
+
`PROTEUM ${version}`,
|
|
81
|
+
'Explicit SSR / SEO / TypeScript framework for agent-friendly apps.',
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
sections.push(
|
|
86
|
+
await renderSection(
|
|
87
|
+
'Recommended flow',
|
|
88
|
+
renderRows(proteumRecommendedFlow, { minLabelWidth: 24, maxLabelWidth: 24 }),
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
const groupedCommands = await Promise.all(
|
|
93
|
+
proteumCommandGroups.map(async (group) =>
|
|
94
|
+
renderSection(
|
|
95
|
+
group.title,
|
|
96
|
+
renderRows(
|
|
97
|
+
group.names.map((name) => {
|
|
98
|
+
const command = proteumCommands[name];
|
|
99
|
+
const initNote = name === 'init' ? ` ${getInitAvailabilityNote(initAvailable)}` : '';
|
|
100
|
+
const status = command.status === 'experimental' ? ' Experimental.' : '';
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
label: command.name === 'init' ? command.usage : `proteum ${command.name}`,
|
|
104
|
+
value: `${command.summary}${status}${initNote}`,
|
|
105
|
+
};
|
|
106
|
+
}),
|
|
107
|
+
{ minLabelWidth: 18, maxLabelWidth: 24 },
|
|
108
|
+
),
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
sections.push(groupedCommands.join('\n\n'));
|
|
114
|
+
|
|
115
|
+
if (!isLikelyProteumAppRoot(workdir)) {
|
|
116
|
+
sections.push(
|
|
117
|
+
await renderSection(
|
|
118
|
+
'Context',
|
|
119
|
+
renderRows([
|
|
120
|
+
{ label: 'current directory', value: workdir },
|
|
121
|
+
{
|
|
122
|
+
label: 'note',
|
|
123
|
+
value: 'This directory does not look like a Proteum app root. Run dev, refresh, build, check, doctor, and explain inside an app where `client/` and `server/` exist.',
|
|
124
|
+
},
|
|
125
|
+
]),
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
sections.push(
|
|
131
|
+
await renderSection(
|
|
132
|
+
'Next',
|
|
133
|
+
[
|
|
134
|
+
wrapText('Run `proteum <command> --help` or `proteum help <command>` for full options and examples.', {
|
|
135
|
+
indent: ' ',
|
|
136
|
+
nextIndent: ' ',
|
|
137
|
+
}),
|
|
138
|
+
wrapText('Legacy single-dash flags and positional booleans remain accepted for older app scripts, but new docs should prefer modern long flags.', {
|
|
139
|
+
indent: ' ',
|
|
140
|
+
nextIndent: ' ',
|
|
141
|
+
}),
|
|
142
|
+
wrapText('Add `--verbose` when you want compiler internals, watch-cycle chatter, and framework setup logs.', {
|
|
143
|
+
indent: ' ',
|
|
144
|
+
nextIndent: ' ',
|
|
145
|
+
}),
|
|
146
|
+
].join('\n'),
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
return `${sections.join('\n\n')}\n`;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export const renderCommandHelp = async ({
|
|
154
|
+
commandName,
|
|
155
|
+
definition,
|
|
156
|
+
workdir,
|
|
157
|
+
initAvailable,
|
|
158
|
+
}: {
|
|
159
|
+
commandName: TProteumCommandName;
|
|
160
|
+
definition: TCommandDefinition;
|
|
161
|
+
workdir: string;
|
|
162
|
+
initAvailable: boolean;
|
|
163
|
+
}) => {
|
|
164
|
+
const command = proteumCommands[commandName];
|
|
165
|
+
const sections: string[] = [];
|
|
166
|
+
const notes = [...(command.notes ?? [])];
|
|
167
|
+
|
|
168
|
+
if (commandName === 'init') notes.push(getInitAvailabilityNote(initAvailable));
|
|
169
|
+
if (commandName !== 'init' && !isLikelyProteumAppRoot(workdir)) {
|
|
170
|
+
notes.push(
|
|
171
|
+
'This command expects to run inside a Proteum app root. The current directory does not contain the usual `client/` and `server/` folders.',
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
sections.push(await renderTitle(`PROTEUM ${command.name.toUpperCase()}`, command.summary));
|
|
176
|
+
sections.push(await renderSection('Usage', ` ${command.usage}`));
|
|
177
|
+
sections.push(
|
|
178
|
+
await renderSection(
|
|
179
|
+
'Category',
|
|
180
|
+
wrapText(`${command.category}${command.status === 'experimental' ? ' · experimental' : ''}`, {
|
|
181
|
+
indent: ' ',
|
|
182
|
+
nextIndent: ' ',
|
|
183
|
+
}),
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
sections.push(await renderSection('Best for', wrapText(command.bestFor, { indent: ' ', nextIndent: ' ' })));
|
|
187
|
+
sections.push(await renderSection('Options', renderOptions(definition)));
|
|
188
|
+
sections.push(await renderSection('Examples', renderExamples(command.examples)));
|
|
189
|
+
|
|
190
|
+
if (notes.length > 0) sections.push(await renderSection('Notes', renderNotes(notes)));
|
|
191
|
+
|
|
192
|
+
return `${sections.join('\n\n')}\n`;
|
|
193
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const React = require('react') as typeof import('react');
|
|
2
|
+
|
|
3
|
+
import { importEsm } from '../runtime/importEsm';
|
|
4
|
+
import { getTerminalWidth } from './layout';
|
|
5
|
+
|
|
6
|
+
type TInkModule = typeof import('ink');
|
|
7
|
+
type TInkUiModule = typeof import('@inkjs/ui');
|
|
8
|
+
|
|
9
|
+
type TInkRuntime = {
|
|
10
|
+
Box: TInkModule['Box'];
|
|
11
|
+
Text: TInkModule['Text'];
|
|
12
|
+
renderToString: TInkModule['renderToString'];
|
|
13
|
+
StatusMessage: TInkUiModule['StatusMessage'];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
let inkRuntimePromise: Promise<TInkRuntime> | undefined;
|
|
17
|
+
|
|
18
|
+
const loadInkRuntime = () => {
|
|
19
|
+
if (inkRuntimePromise) return inkRuntimePromise;
|
|
20
|
+
|
|
21
|
+
inkRuntimePromise = Promise.all([
|
|
22
|
+
importEsm<TInkModule>('ink'),
|
|
23
|
+
importEsm<TInkUiModule>('@inkjs/ui'),
|
|
24
|
+
]).then(([ink, inkUi]) => ({
|
|
25
|
+
Box: ink.Box,
|
|
26
|
+
Text: ink.Text,
|
|
27
|
+
renderToString: ink.renderToString,
|
|
28
|
+
StatusMessage: inkUi.StatusMessage,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
return inkRuntimePromise;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const renderInk = async (
|
|
35
|
+
buildNode: (runtime: TInkRuntime) => import('react').ReactElement | null,
|
|
36
|
+
columns = getTerminalWidth(),
|
|
37
|
+
) => {
|
|
38
|
+
const runtime = await loadInkRuntime();
|
|
39
|
+
return runtime.renderToString(buildNode(runtime), { columns });
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const renderTitle = async (title: string, subtitle?: string) =>
|
|
43
|
+
renderInk(({ Box, Text }) => {
|
|
44
|
+
const createElement = React.createElement;
|
|
45
|
+
|
|
46
|
+
return createElement(
|
|
47
|
+
Box,
|
|
48
|
+
{ flexDirection: 'column' },
|
|
49
|
+
createElement(Text, { bold: true, color: 'cyan' }, title),
|
|
50
|
+
subtitle ? createElement(Text, { dimColor: true }, subtitle) : null,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const renderSection = async (title: string, body: string) => {
|
|
55
|
+
const heading = await renderInk(({ Text }) => React.createElement(Text, { bold: true }, title));
|
|
56
|
+
return `${heading}\n${body}`;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const renderStep = async (label: string, message: string) =>
|
|
60
|
+
renderInk(({ Text }) => React.createElement(Text, { color: 'cyan' }, `${label} ${message}`));
|
|
61
|
+
|
|
62
|
+
const renderStatusMessage = async (variant: 'success' | 'warning' | 'error', message: string) =>
|
|
63
|
+
renderInk(({ StatusMessage }) => React.createElement(StatusMessage, { variant }, message));
|
|
64
|
+
|
|
65
|
+
export const renderSuccess = (message: string) => renderStatusMessage('success', message);
|
|
66
|
+
|
|
67
|
+
export const renderWarning = (message: string) => renderStatusMessage('warning', message);
|
|
68
|
+
|
|
69
|
+
export const renderDanger = (message: string) => renderStatusMessage('error', message);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const ansiPattern = /\u001b\[[0-9;]*m/g;
|
|
2
|
+
|
|
3
|
+
export type TRow = {
|
|
4
|
+
label: string;
|
|
5
|
+
value: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const stripAnsi = (value: string) => value.replace(ansiPattern, '');
|
|
9
|
+
|
|
10
|
+
const visibleLength = (value: string) => stripAnsi(value).length;
|
|
11
|
+
|
|
12
|
+
export const getTerminalWidth = () => {
|
|
13
|
+
const width = process.stdout.columns ?? 100;
|
|
14
|
+
return Math.max(72, Math.min(width, 110));
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const wrapText = (
|
|
18
|
+
value: string,
|
|
19
|
+
{
|
|
20
|
+
width = getTerminalWidth(),
|
|
21
|
+
indent = '',
|
|
22
|
+
nextIndent = indent,
|
|
23
|
+
}: {
|
|
24
|
+
width?: number;
|
|
25
|
+
indent?: string;
|
|
26
|
+
nextIndent?: string;
|
|
27
|
+
} = {},
|
|
28
|
+
) => {
|
|
29
|
+
const words = value.trim().split(/\s+/).filter(Boolean);
|
|
30
|
+
if (words.length === 0) return indent.trimEnd();
|
|
31
|
+
|
|
32
|
+
const lines: string[] = [];
|
|
33
|
+
let currentLine = indent;
|
|
34
|
+
|
|
35
|
+
for (const word of words) {
|
|
36
|
+
const separator = currentLine.trim().length === 0 ? '' : ' ';
|
|
37
|
+
const nextLine = `${currentLine}${separator}${word}`;
|
|
38
|
+
|
|
39
|
+
if (visibleLength(nextLine) > width && currentLine.trim().length > 0) {
|
|
40
|
+
lines.push(currentLine);
|
|
41
|
+
currentLine = `${nextIndent}${word}`;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
currentLine = nextLine;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
lines.push(currentLine);
|
|
49
|
+
return lines.join('\n');
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const renderRows = (
|
|
53
|
+
rows: TRow[],
|
|
54
|
+
{
|
|
55
|
+
indent = ' ',
|
|
56
|
+
minLabelWidth = 14,
|
|
57
|
+
maxLabelWidth = 28,
|
|
58
|
+
}: {
|
|
59
|
+
indent?: string;
|
|
60
|
+
minLabelWidth?: number;
|
|
61
|
+
maxLabelWidth?: number;
|
|
62
|
+
} = {},
|
|
63
|
+
) => {
|
|
64
|
+
if (rows.length === 0) return `${indent}none`;
|
|
65
|
+
|
|
66
|
+
const width = getTerminalWidth();
|
|
67
|
+
const labelWidth = Math.min(
|
|
68
|
+
maxLabelWidth,
|
|
69
|
+
Math.max(
|
|
70
|
+
minLabelWidth,
|
|
71
|
+
...rows.map((row) => visibleLength(row.label)),
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return rows
|
|
76
|
+
.map((row) => {
|
|
77
|
+
const paddedLabel = `${row.label}${' '.repeat(Math.max(labelWidth - visibleLength(row.label), 0))}`;
|
|
78
|
+
const prefix = `${indent}${paddedLabel} `;
|
|
79
|
+
const continuation = `${indent}${' '.repeat(labelWidth)} `;
|
|
80
|
+
return wrapText(row.value, { width, indent: prefix, nextIndent: continuation });
|
|
81
|
+
})
|
|
82
|
+
.join('\n');
|
|
83
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { UsageError } from 'clipanion';
|
|
2
|
+
|
|
3
|
+
import type { TArgsObject } from '../context';
|
|
4
|
+
|
|
5
|
+
export const normalizeLegacyArgv = (argv: string[]) =>
|
|
6
|
+
argv.map((arg) => {
|
|
7
|
+
if (!/^-[-A-Za-z0-9]+$/.test(arg)) return arg;
|
|
8
|
+
if (arg.startsWith('--')) return arg;
|
|
9
|
+
if (arg.length <= 2) return arg;
|
|
10
|
+
|
|
11
|
+
return `--${arg.substring(1)}`;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const normalizeHelpArgv = (argv: string[], commandNames: readonly string[]) => {
|
|
15
|
+
if (argv.length === 0) return argv;
|
|
16
|
+
if (!commandNames.includes(argv[0])) return argv;
|
|
17
|
+
if (!argv.includes('--help') && !argv.includes('-h')) return argv;
|
|
18
|
+
|
|
19
|
+
return [argv[0], '--help'];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const createArgs = (args: TArgsObject = {}) => ({ workdir: process.cwd(), ...args });
|
|
23
|
+
|
|
24
|
+
export const applyLegacyBooleanArgs = (
|
|
25
|
+
commandName: string,
|
|
26
|
+
legacyArgs: readonly string[],
|
|
27
|
+
allowedArgs: readonly string[],
|
|
28
|
+
args: TArgsObject,
|
|
29
|
+
) => {
|
|
30
|
+
const allowedArgsSet = new Set(allowedArgs);
|
|
31
|
+
|
|
32
|
+
for (const legacyArg of legacyArgs) {
|
|
33
|
+
if (!allowedArgsSet.has(legacyArg)) {
|
|
34
|
+
throw new UsageError(
|
|
35
|
+
`Unknown ${commandName} argument: ${legacyArg}. Allowed values: ${allowedArgs.join(', ') || 'none'}.`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
args[legacyArg] = true;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const assertNoLegacyArgs = (commandName: string, legacyArgs: readonly string[]) => {
|
|
44
|
+
if (legacyArgs.length === 0) return;
|
|
45
|
+
|
|
46
|
+
throw new UsageError(
|
|
47
|
+
`${commandName} does not accept positional arguments. Received: ${legacyArgs.join(', ')}.`,
|
|
48
|
+
);
|
|
49
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Command, Option } from 'clipanion';
|
|
2
|
+
|
|
3
|
+
import cli, { type TArgsObject } from '../context';
|
|
4
|
+
import { createClipanionUsage, proteumCommands, type TProteumCommandName } from '../presentation/commands';
|
|
5
|
+
import { createArgs } from './argv';
|
|
6
|
+
|
|
7
|
+
type TRunModule = { run: () => Promise<void> };
|
|
8
|
+
|
|
9
|
+
export const runCommandModule = async (loader: () => Promise<TRunModule>) => {
|
|
10
|
+
const module = await loader();
|
|
11
|
+
await module.run();
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export abstract class ProteumCommand extends Command {
|
|
15
|
+
public verbose = Option.Boolean('-v,--verbose', false, {
|
|
16
|
+
description: 'Show verbose compiler, watcher, and framework setup logs.',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
protected setCliArgs(args: TArgsObject = {}) {
|
|
20
|
+
cli.setArgs(createArgs({ ...args, verbose: this.verbose }));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const buildUsage = (commandName: TProteumCommandName) =>
|
|
25
|
+
Command.Usage(createClipanionUsage(proteumCommands[commandName]));
|