@vobs/cli 0.1.0 → 0.2.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/ai-server.d.ts +11 -0
- package/dist/ai-server.js +212 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +301 -0
- package/dist/diagnose.d.ts +12 -0
- package/dist/diagnose.js +122 -0
- package/dist/enterprise-template.d.ts +4 -4
- package/dist/enterprise-template.js +18 -39
- package/dist/index.d.ts +2 -1
- package/dist/index.js +245 -3
- package/dist/inspect.d.ts +11 -0
- package/dist/inspect.js +576 -0
- package/dist/scaffold.js +10 -14
- package/package.json +8 -6
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare const enterpriseFiles: readonly [{
|
|
6
6
|
readonly path: "package.json";
|
|
7
|
-
readonly content: "{\n \"name\": \"{{packageName}}\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc --noEmit && vite build\",\n \"preview\": \"vite preview\",\n \"typecheck\": \"tsc --noEmit\"\n },\n \"dependencies\": {\n \"@vobs/di\": \"latest\",\n \"@vobs/forms\": \"latest\",\n \"@vobs/i18n\": \"latest\",\n \"@vobs/reactivity\": \"latest\",\n \"@vobs/router\": \"latest\",\n \"@vobs/runtime-dom\": \"latest\",\n \"@vobs/server-renderer\": \"latest\",\n \"@vobs/ui\": \"latest\"\n },\n \"devDependencies\": {\n \"@vobs/vite-plugin\": \"latest\",\n \"typescript\": \"^5.7.0\",\n \"vite\": \"^7.0.0\"\n }\n}\n";
|
|
7
|
+
readonly content: "{\n \"name\": \"{{packageName}}\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"scripts\": {\n \"dev\": \"vite\",\n \"build\": \"tsc --noEmit && vite build\",\n \"preview\": \"vite preview\",\n \"typecheck\": \"tsc --noEmit\"\n },\n \"dependencies\": {\n \"@vobs/di\": \"latest\",\n \"@vobs/authorization\": \"latest\",\n \"@vobs/forms\": \"latest\",\n \"@vobs/i18n\": \"latest\",\n \"@vobs/reactivity\": \"latest\",\n \"@vobs/router\": \"latest\",\n \"@vobs/runtime-dom\": \"latest\",\n \"@vobs/server-renderer\": \"latest\",\n \"@vobs/ui\": \"latest\"\n },\n \"devDependencies\": {\n \"@vobs/vite-plugin\": \"latest\",\n \"typescript\": \"^5.7.0\",\n \"vite\": \"^7.0.0\"\n }\n}\n";
|
|
8
8
|
}, {
|
|
9
9
|
readonly path: "tsconfig.json";
|
|
10
10
|
readonly content: "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"ESNext\",\n \"moduleResolution\": \"Bundler\",\n \"lib\": [\"ES2022\", \"DOM\", \"DOM.Iterable\"],\n \"strict\": true,\n \"skipLibCheck\": true,\n \"isolatedModules\": true,\n \"verbatimModuleSyntax\": true,\n \"types\": [\"@vobs/vite-plugin/client\"]\n },\n \"include\": [\"src/**/*.ts\", \"vite.config.ts\"]\n}\n";
|
|
@@ -16,7 +16,7 @@ export declare const enterpriseFiles: readonly [{
|
|
|
16
16
|
readonly content: "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>{{projectName}}</title>\n </head>\n <body>\n <div id=\"app\"></div>\n <script type=\"module\" src=\"/src/main.ts\"></script>\n </body>\n</html>\n";
|
|
17
17
|
}, {
|
|
18
18
|
readonly path: "README.md";
|
|
19
|
-
readonly content: "# {{projectName}}\n\n{{description}}\n\n## Run\n\n~~~bash\
|
|
19
|
+
readonly content: "# {{projectName}}\n\n{{description}}\n\n## Run\n\n~~~bash\npnpm install\npnpm run dev\n~~~\n\nOpen /dashboard for the application shell and /dashboard/users/new for the schema-driven form.\n\n## Boundaries\n\n| Area | Starter responsibility | Production responsibility |\n| --- | --- | --- |\n| DI | One root InjectionScope wires authorization, audit, UI, and i18n | Replace service implementations at composition time |\n| Authorization | Route guards consume an AuthorizationSubject and capability list | Authenticate the request and re-authorize every server mutation |\n| UI | setupUi registers AppShell and shared providers | Extend navigation and components without putting policy decisions in UI Plans |\n| Forms | useForm owns validation and accessible view state | Call an API and apply server errors before showing success |\n| SSR | render-request.ts adapts URL/request data to renderRoute | Supply compiled routes/templates per request and attach platform HTTP headers |\n\nThe built-in local subject exists only so the generated client demo is navigable. Treat client capability checks as presentation logic, never as a security boundary. On SSR, pass authorizationSubject in the request context. Your API must independently authenticate and authorize writes.\n\nThe SSR adapter is host-independent. A Node, Hono, Express, or edge entry point can call renderEnterpriseRequest with its route array, server template registry, per-request environment, and request context, then map RenderRouteResult.status to an HTTP status. Keep secrets and non-serializable session objects out of initialState.\n";
|
|
20
20
|
}, {
|
|
21
21
|
readonly path: "src/env.d.ts";
|
|
22
22
|
readonly content: "/// <reference types=\"@vobs/vite-plugin/client\" />\n\ndeclare module '*.css';\n";
|
|
@@ -31,13 +31,13 @@ export declare const enterpriseFiles: readonly [{
|
|
|
31
31
|
readonly content: "import { createI18n, defineMessages } from '@vobs/i18n';\n\nconst en = defineMessages({\n navigation: {\n dashboard: 'Dashboard',\n users: 'Users',\n },\n dashboard: {\n title: 'Operations dashboard',\n },\n users: {\n new: 'Create user',\n },\n forms: {\n required: '{label} is required',\n },\n});\n\nexport function createEnterpriseI18n() {\n return createI18n({\n locale: 'en-US',\n fallbackLocale: 'en-US',\n messages: { 'en-US': en },\n });\n}\n";
|
|
32
32
|
}, {
|
|
33
33
|
readonly path: "src/app/services.ts";
|
|
34
|
-
readonly content: "import { createInjectionKey, type InjectionScope } from '@vobs/di';\nimport {
|
|
34
|
+
readonly content: "import { createInjectionKey, type InjectionScope } from '@vobs/di';\nimport { AuthorizationPortKey, authorizer } from '../access/authorization.js';\n\nexport interface AuditService {\n record(event: string, details: Readonly<Record<string, unknown>>): void;\n}\n\nexport const AuditServiceKey = createInjectionKey<AuditService>('enterprise.audit');\n\nexport function registerEnterpriseServices(scope: InjectionScope): void {\n scope.provide(AuthorizationPortKey, authorizer);\n scope.provide(AuditServiceKey, {\n record(event, details) {\n console.info('[audit]', event, details);\n },\n });\n}\n";
|
|
35
35
|
}, {
|
|
36
36
|
readonly path: "src/app-shell.ts";
|
|
37
37
|
readonly content: "import { defineAppShell } from '@vobs/ui';\n\nexport default defineAppShell({\n id: 'enterprise',\n title: '{{projectName}}',\n brand: {\n label: '{{projectName}}',\n href: '/dashboard',\n mark: 'K',\n },\n navigation: {\n primary: [\n { id: 'dashboard', label: 'Dashboard', route: '/dashboard' },\n { id: 'new-user', label: 'Create user', route: '/dashboard/users/new' },\n ],\n },\n routes: [\n {\n route: '/dashboard',\n title: 'Operations dashboard',\n description: 'A quiet starting point for enterprise workflows.',\n activeNav: 'dashboard',\n status: 'Ready',\n },\n {\n route: '/dashboard/users/new',\n title: 'Create user',\n description: 'Schema-driven form with DI, i18n, and validation.',\n activeNav: 'new-user',\n status: 'Form',\n },\n ],\n pageHeader: {\n kicker: 'Workspace',\n titleFallback: '{{projectName}}',\n descriptionFallback: 'Enterprise administration starter.',\n },\n topbar: {\n account: {\n label: 'Local Admin',\n initials: 'LA',\n menu: [{ id: 'sign-out', label: 'Sign out', tone: 'danger' }],\n },\n },\n statusbar: {\n text: 'Ready',\n version: '{{projectName}} v0.1.0',\n },\n theme: {\n mode: 'light',\n brand: 'green',\n },\n});\n";
|
|
38
38
|
}, {
|
|
39
39
|
readonly path: "src/access/authorization.ts";
|
|
40
|
-
readonly content: "import {
|
|
40
|
+
readonly content: "import {\n AuthorizationPortKey,\n createAuthorizer,\n type AuthorizationSubject,\n} from '@vobs/authorization';\nimport type { RouteGuardContext, RouteGuardResult } from '@vobs/router';\n\nexport interface AuthorizationRequestContext {\n readonly authorizationSubject?: AuthorizationSubject | null;\n}\n\nexport const authorizer = createAuthorizer({\n roles: [{ name: 'local-admin', permissions: ['dashboard:view', 'users:write'] }],\n});\n\nconst developmentSubject: AuthorizationSubject = {\n id: 'local-admin',\n roles: ['local-admin'],\n};\n\nexport function requireCapabilities(\n context: RouteGuardContext,\n requiredCapabilities: readonly string[],\n): RouteGuardResult {\n const subject = readAuthorizationSubject(context.request);\n if (subject === null) return '/login';\n const allowed = requiredCapabilities.every((capability) =>\n authorizer.can(subject, capability, { type: 'route', attributes: { path: context.route.path } }),\n );\n return allowed ? true : '/403';\n}\n\nfunction readAuthorizationSubject(request: unknown): AuthorizationSubject | null {\n if (typeof request !== 'object' || request === null || !('authorizationSubject' in request)) {\n return developmentSubject;\n }\n return (request as AuthorizationRequestContext).authorizationSubject ?? null;\n}\n";
|
|
41
41
|
}, {
|
|
42
42
|
readonly path: "src/server/render-request.ts";
|
|
43
43
|
readonly content: "import { createRouteResolver } from '@vobs/router';\nimport {\n renderRoute,\n type RenderRouteOptions,\n type RenderRouteResult,\n} from '@vobs/server-renderer';\n\nexport interface EnterpriseSsrRequestOptions\n extends Omit<RenderRouteOptions, 'path' | 'request' | 'resolver'> {\n readonly url: string;\n readonly request?: unknown;\n}\n\nexport async function renderEnterpriseRequest(\n options: EnterpriseSsrRequestOptions,\n): Promise<RenderRouteResult> {\n const { url, request, ...renderOptions } = options;\n const location = new URL(url, 'http://localhost');\n return renderRoute({\n ...renderOptions,\n path: location.pathname,\n resolver: createRouteResolver(),\n ...(request === undefined ? {} : { request }),\n });\n}\n\nexport function createHtmlDocument(result: RenderRouteResult): string {\n return (\n '<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\">' +\n '<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">' +\n '<title>{{projectName}}</title></head><body><div id=\"app\">' +\n result.html +\n (result.initialStateScript ?? '') +\n '</div><script type=\"module\" src=\"/src/main.ts\"></script></body></html>'\n );\n}\n";
|
|
@@ -15,6 +15,7 @@ const packageJson = `{
|
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@vobs/di": "latest",
|
|
18
|
+
"@vobs/authorization": "latest",
|
|
18
19
|
"@vobs/forms": "latest",
|
|
19
20
|
"@vobs/i18n": "latest",
|
|
20
21
|
"@vobs/reactivity": "latest",
|
|
@@ -131,10 +132,7 @@ export function createEnterpriseI18n() {
|
|
|
131
132
|
}
|
|
132
133
|
`;
|
|
133
134
|
const servicesTs = `import { createInjectionKey, type InjectionScope } from '@vobs/di';
|
|
134
|
-
import {
|
|
135
|
-
AuthorizationPortKey,
|
|
136
|
-
defaultAuthorizationPort,
|
|
137
|
-
} from '../access/authorization.js';
|
|
135
|
+
import { AuthorizationPortKey, authorizer } from '../access/authorization.js';
|
|
138
136
|
|
|
139
137
|
export interface AuditService {
|
|
140
138
|
record(event: string, details: Readonly<Record<string, unknown>>): void;
|
|
@@ -143,7 +141,7 @@ export interface AuditService {
|
|
|
143
141
|
export const AuditServiceKey = createInjectionKey<AuditService>('enterprise.audit');
|
|
144
142
|
|
|
145
143
|
export function registerEnterpriseServices(scope: InjectionScope): void {
|
|
146
|
-
scope.provide(AuthorizationPortKey,
|
|
144
|
+
scope.provide(AuthorizationPortKey, authorizer);
|
|
147
145
|
scope.provide(AuditServiceKey, {
|
|
148
146
|
record(event, details) {
|
|
149
147
|
console.info('[audit]', event, details);
|
|
@@ -151,45 +149,24 @@ export function registerEnterpriseServices(scope: InjectionScope): void {
|
|
|
151
149
|
});
|
|
152
150
|
}
|
|
153
151
|
`;
|
|
154
|
-
const authorizationTs = `import {
|
|
152
|
+
const authorizationTs = `import {
|
|
153
|
+
AuthorizationPortKey,
|
|
154
|
+
createAuthorizer,
|
|
155
|
+
type AuthorizationSubject,
|
|
156
|
+
} from '@vobs/authorization';
|
|
155
157
|
import type { RouteGuardContext, RouteGuardResult } from '@vobs/router';
|
|
156
158
|
|
|
157
|
-
export interface AuthorizationSubject {
|
|
158
|
-
readonly id: string;
|
|
159
|
-
readonly capabilities: readonly string[];
|
|
160
|
-
}
|
|
161
|
-
|
|
162
159
|
export interface AuthorizationRequestContext {
|
|
163
160
|
readonly authorizationSubject?: AuthorizationSubject | null;
|
|
164
161
|
}
|
|
165
162
|
|
|
166
|
-
export
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
export interface AuthorizationPort {
|
|
172
|
-
evaluate(
|
|
173
|
-
subject: AuthorizationSubject | null,
|
|
174
|
-
requiredCapabilities: readonly string[],
|
|
175
|
-
): AuthorizationDecision;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export const AuthorizationPortKey =
|
|
179
|
-
createInjectionKey<AuthorizationPort>('enterprise.authorization');
|
|
180
|
-
|
|
181
|
-
export const defaultAuthorizationPort: AuthorizationPort = {
|
|
182
|
-
evaluate(subject, requiredCapabilities) {
|
|
183
|
-
const missingCapabilities = requiredCapabilities.filter(
|
|
184
|
-
(capability) => subject?.capabilities.includes(capability) !== true,
|
|
185
|
-
);
|
|
186
|
-
return { allowed: subject !== null && missingCapabilities.length === 0, missingCapabilities };
|
|
187
|
-
},
|
|
188
|
-
};
|
|
163
|
+
export const authorizer = createAuthorizer({
|
|
164
|
+
roles: [{ name: 'local-admin', permissions: ['dashboard:view', 'users:write'] }],
|
|
165
|
+
});
|
|
189
166
|
|
|
190
167
|
const developmentSubject: AuthorizationSubject = {
|
|
191
168
|
id: 'local-admin',
|
|
192
|
-
|
|
169
|
+
roles: ['local-admin'],
|
|
193
170
|
};
|
|
194
171
|
|
|
195
172
|
export function requireCapabilities(
|
|
@@ -198,8 +175,10 @@ export function requireCapabilities(
|
|
|
198
175
|
): RouteGuardResult {
|
|
199
176
|
const subject = readAuthorizationSubject(context.request);
|
|
200
177
|
if (subject === null) return '/login';
|
|
201
|
-
const
|
|
202
|
-
|
|
178
|
+
const allowed = requiredCapabilities.every((capability) =>
|
|
179
|
+
authorizer.can(subject, capability, { type: 'route', attributes: { path: context.route.path } }),
|
|
180
|
+
);
|
|
181
|
+
return allowed ? true : '/403';
|
|
203
182
|
}
|
|
204
183
|
|
|
205
184
|
function readAuthorizationSubject(request: unknown): AuthorizationSubject | null {
|
|
@@ -606,8 +585,8 @@ const readme = `# {{projectName}}
|
|
|
606
585
|
## Run
|
|
607
586
|
|
|
608
587
|
~~~bash
|
|
609
|
-
|
|
610
|
-
|
|
588
|
+
pnpm install
|
|
589
|
+
pnpm run dev
|
|
611
590
|
~~~
|
|
612
591
|
|
|
613
592
|
Open /dashboard for the application shell and /dashboard/users/new for the schema-driven form.
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
* Copyright (c) 2026 vobsjs
|
|
4
4
|
* @vobs/cli
|
|
5
5
|
*/
|
|
6
|
+
export { runVobsAiServer, type VobsAiServerStreams } from './ai-server.js';
|
|
6
7
|
interface CliStreams {
|
|
8
|
+
readonly stdin?: NodeJS.ReadableStream;
|
|
7
9
|
readonly stdout: Pick<NodeJS.WriteStream, 'write'>;
|
|
8
10
|
readonly stderr: Pick<NodeJS.WriteStream, 'write'>;
|
|
9
11
|
readonly prompt?: CliPrompt;
|
|
@@ -14,4 +16,3 @@ interface CliPrompt {
|
|
|
14
16
|
close?(): void;
|
|
15
17
|
}
|
|
16
18
|
export declare function runVobsCli(args: readonly string[], streams?: CliStreams): Promise<number>;
|
|
17
|
-
export {};
|
package/dist/index.js
CHANGED
|
@@ -10,9 +10,19 @@ import process from 'node:process';
|
|
|
10
10
|
import { createInterface } from 'node:readline/promises';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { checkViewProject } from '@vobs/compiler-dom';
|
|
13
|
+
import { VOBS_AI_TOOL_CATALOG } from '@vobs/ai-contracts';
|
|
14
|
+
import { createVobsProjectContext } from './context.js';
|
|
15
|
+
import { createVobsDiagnosticReport } from './diagnose.js';
|
|
16
|
+
import { inspectVobsProject, writeInspectOutput } from './inspect.js';
|
|
17
|
+
import { runVobsAiServer } from './ai-server.js';
|
|
18
|
+
export { runVobsAiServer } from './ai-server.js';
|
|
13
19
|
import { renderCliLogo } from './logo/cli-logo.js';
|
|
14
20
|
import { createScaffold, isScaffoldTemplate, scaffoldSuccessText, } from './scaffold.js';
|
|
15
|
-
export function runVobsCli(args, streams = {
|
|
21
|
+
export function runVobsCli(args, streams = {
|
|
22
|
+
stdin: process.stdin,
|
|
23
|
+
stdout: process.stdout,
|
|
24
|
+
stderr: process.stderr,
|
|
25
|
+
}) {
|
|
16
26
|
const { args: commandArgs, shouldShowLogo } = readLogoOption(args);
|
|
17
27
|
if (shouldShowLogo) {
|
|
18
28
|
streams.stdout.write(`${renderCliLogo()}\n\n`);
|
|
@@ -25,6 +35,24 @@ export function runVobsCli(args, streams = { stdout: process.stdout, stderr: pro
|
|
|
25
35
|
if (parsed.command === 'new') {
|
|
26
36
|
return runNewCommand(parsed, streams);
|
|
27
37
|
}
|
|
38
|
+
if (parsed.command === 'inspect') {
|
|
39
|
+
return runInspectCommand(parsed, streams);
|
|
40
|
+
}
|
|
41
|
+
if (parsed.command === 'ai-tools') {
|
|
42
|
+
streams.stdout.write(`${JSON.stringify(VOBS_AI_TOOL_CATALOG, null, 2)}\n`);
|
|
43
|
+
return Promise.resolve(0);
|
|
44
|
+
}
|
|
45
|
+
if (parsed.command === 'ai-serve') {
|
|
46
|
+
return runAiServeCommand(streams);
|
|
47
|
+
}
|
|
48
|
+
if (parsed.command === 'context') {
|
|
49
|
+
streams.stdout.write(`${JSON.stringify(createVobsProjectContext(parsed.options), null, 2)}\n`);
|
|
50
|
+
return Promise.resolve(0);
|
|
51
|
+
}
|
|
52
|
+
if (parsed.command === 'diagnose') {
|
|
53
|
+
streams.stdout.write(`${JSON.stringify(createVobsDiagnosticReport(parsed.options), null, 2)}\n`);
|
|
54
|
+
return Promise.resolve(0);
|
|
55
|
+
}
|
|
28
56
|
const result = checkViewProject({
|
|
29
57
|
tsconfigPath: parsed.tsconfigPath,
|
|
30
58
|
...(parsed.viewFiles === undefined ? {} : { viewFiles: parsed.viewFiles }),
|
|
@@ -42,9 +70,14 @@ export function runVobsCli(args, streams = { stdout: process.stdout, stderr: pro
|
|
|
42
70
|
return Promise.resolve(1);
|
|
43
71
|
}
|
|
44
72
|
function readLogoOption(args) {
|
|
73
|
+
const commandArgs = args.filter((arg) => arg !== '--no-logo');
|
|
74
|
+
const isAiServer = commandArgs[0] === 'ai-serve' || (commandArgs[0] === 'ai' && commandArgs[1] === 'serve');
|
|
45
75
|
return {
|
|
46
|
-
args:
|
|
47
|
-
shouldShowLogo: !args.includes('--no-logo')
|
|
76
|
+
args: commandArgs,
|
|
77
|
+
shouldShowLogo: !args.includes('--no-logo') &&
|
|
78
|
+
!args.includes('--json') &&
|
|
79
|
+
!args.includes('inspect') &&
|
|
80
|
+
!isAiServer,
|
|
48
81
|
};
|
|
49
82
|
}
|
|
50
83
|
function parseArgs(args) {
|
|
@@ -53,6 +86,24 @@ function parseArgs(args) {
|
|
|
53
86
|
return { command: 'help' };
|
|
54
87
|
if (command === 'new' || command === 'create')
|
|
55
88
|
return parseNewArgs(rest, command === 'create');
|
|
89
|
+
if (command === 'inspect')
|
|
90
|
+
return parseInspectArgs(rest);
|
|
91
|
+
if (command === 'ai' && rest[0] === 'tools')
|
|
92
|
+
return parseAiToolsArgs(rest.slice(1));
|
|
93
|
+
if (command === 'ai' && rest[0] === 'serve')
|
|
94
|
+
return parseAiServeArgs(rest.slice(1));
|
|
95
|
+
if (command === 'ai' && rest[0] === 'context')
|
|
96
|
+
return parseContextArgs(rest.slice(1));
|
|
97
|
+
if (command === 'ai' && rest[0] === 'diagnose')
|
|
98
|
+
return parseDiagnoseArgs(rest.slice(1));
|
|
99
|
+
if (command === 'ai-tools')
|
|
100
|
+
return parseAiToolsArgs(rest);
|
|
101
|
+
if (command === 'ai-serve')
|
|
102
|
+
return parseAiServeArgs(rest);
|
|
103
|
+
if (command === 'context')
|
|
104
|
+
return parseContextArgs(rest);
|
|
105
|
+
if (command === 'diagnose')
|
|
106
|
+
return parseDiagnoseArgs(rest);
|
|
56
107
|
if (command !== 'check')
|
|
57
108
|
throw new Error(`Unknown command: ${command}`);
|
|
58
109
|
let tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
|
|
@@ -85,6 +136,192 @@ function parseArgs(args) {
|
|
|
85
136
|
viewFiles: viewFiles.length === 0 ? undefined : viewFiles,
|
|
86
137
|
};
|
|
87
138
|
}
|
|
139
|
+
function parseAiServeArgs(args) {
|
|
140
|
+
for (const arg of args) {
|
|
141
|
+
if (arg === '--help' || arg === '-h')
|
|
142
|
+
return { command: 'help' };
|
|
143
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
144
|
+
}
|
|
145
|
+
return { command: 'ai-serve' };
|
|
146
|
+
}
|
|
147
|
+
function parseAiToolsArgs(args) {
|
|
148
|
+
for (const arg of args) {
|
|
149
|
+
if (arg === '--json')
|
|
150
|
+
continue;
|
|
151
|
+
if (arg === '--help' || arg === '-h')
|
|
152
|
+
return { command: 'help' };
|
|
153
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
154
|
+
}
|
|
155
|
+
return { command: 'ai-tools' };
|
|
156
|
+
}
|
|
157
|
+
function parseContextArgs(args) {
|
|
158
|
+
let tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
|
|
159
|
+
let route;
|
|
160
|
+
let file;
|
|
161
|
+
let depth;
|
|
162
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
163
|
+
const arg = args[index];
|
|
164
|
+
if (arg === '--json')
|
|
165
|
+
continue;
|
|
166
|
+
if (arg === '--help' || arg === '-h')
|
|
167
|
+
return { command: 'help' };
|
|
168
|
+
if (arg === '--tsconfig' || arg === '-p') {
|
|
169
|
+
const value = args[index + 1];
|
|
170
|
+
if (value === undefined)
|
|
171
|
+
throw new Error(`${arg} requires a path`);
|
|
172
|
+
tsconfigPath = path.resolve(value);
|
|
173
|
+
index += 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (arg === '--route') {
|
|
177
|
+
const value = args[index + 1];
|
|
178
|
+
if (value === undefined)
|
|
179
|
+
throw new Error('--route requires a path');
|
|
180
|
+
route = value;
|
|
181
|
+
index += 1;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (arg === '--file') {
|
|
185
|
+
const value = args[index + 1];
|
|
186
|
+
if (value === undefined)
|
|
187
|
+
throw new Error('--file requires a path');
|
|
188
|
+
file = value;
|
|
189
|
+
index += 1;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (arg === '--depth') {
|
|
193
|
+
const value = args[index + 1];
|
|
194
|
+
if (value === undefined)
|
|
195
|
+
throw new Error('--depth requires a number');
|
|
196
|
+
depth = Number(value);
|
|
197
|
+
index += 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
throw new Error(`Unknown option: ${arg ?? ''}`);
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
command: 'context',
|
|
204
|
+
options: {
|
|
205
|
+
tsconfigPath,
|
|
206
|
+
...(route === undefined ? {} : { route }),
|
|
207
|
+
...(file === undefined ? {} : { file }),
|
|
208
|
+
...(depth === undefined ? {} : { depth }),
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function parseDiagnoseArgs(args) {
|
|
213
|
+
let tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
|
|
214
|
+
let route;
|
|
215
|
+
let file;
|
|
216
|
+
let depth;
|
|
217
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
218
|
+
const arg = args[index];
|
|
219
|
+
if (arg === '--json')
|
|
220
|
+
continue;
|
|
221
|
+
if (arg === '--help' || arg === '-h')
|
|
222
|
+
return { command: 'help' };
|
|
223
|
+
if (arg === '--tsconfig' || arg === '-p') {
|
|
224
|
+
const value = args[index + 1];
|
|
225
|
+
if (value === undefined)
|
|
226
|
+
throw new Error(`${arg} requires a path`);
|
|
227
|
+
tsconfigPath = path.resolve(value);
|
|
228
|
+
index += 1;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (arg === '--route') {
|
|
232
|
+
const value = args[index + 1];
|
|
233
|
+
if (value === undefined)
|
|
234
|
+
throw new Error('--route requires a path');
|
|
235
|
+
route = value;
|
|
236
|
+
index += 1;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (arg === '--file') {
|
|
240
|
+
const value = args[index + 1];
|
|
241
|
+
if (value === undefined)
|
|
242
|
+
throw new Error('--file requires a path');
|
|
243
|
+
file = value;
|
|
244
|
+
index += 1;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (arg === '--depth') {
|
|
248
|
+
const value = args[index + 1];
|
|
249
|
+
if (value === undefined)
|
|
250
|
+
throw new Error('--depth requires a number');
|
|
251
|
+
depth = Number(value);
|
|
252
|
+
index += 1;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
throw new Error(`Unknown option: ${arg ?? ''}`);
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
command: 'diagnose',
|
|
259
|
+
options: {
|
|
260
|
+
tsconfigPath,
|
|
261
|
+
...(route === undefined ? {} : { route }),
|
|
262
|
+
...(file === undefined ? {} : { file }),
|
|
263
|
+
...(depth === undefined ? {} : { depth }),
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function parseInspectArgs(args) {
|
|
268
|
+
let tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
|
|
269
|
+
let out;
|
|
270
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
271
|
+
const arg = args[index];
|
|
272
|
+
if (arg === '--help' || arg === '-h')
|
|
273
|
+
return { command: 'help' };
|
|
274
|
+
if (arg === '--json')
|
|
275
|
+
continue;
|
|
276
|
+
if (arg === '--tsconfig' || arg === '-p') {
|
|
277
|
+
const value = args[index + 1];
|
|
278
|
+
if (value === undefined)
|
|
279
|
+
throw new Error(`${arg} requires a path`);
|
|
280
|
+
tsconfigPath = path.resolve(value);
|
|
281
|
+
index += 1;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (arg === '--out' || arg === '-o') {
|
|
285
|
+
const value = args[index + 1];
|
|
286
|
+
if (value === undefined)
|
|
287
|
+
throw new Error(`${arg} requires a path`);
|
|
288
|
+
out = value;
|
|
289
|
+
index += 1;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
throw new Error(`Unknown option: ${arg ?? ''}`);
|
|
293
|
+
}
|
|
294
|
+
return { command: 'inspect', options: { tsconfigPath, ...(out === undefined ? {} : { out }) } };
|
|
295
|
+
}
|
|
296
|
+
function runInspectCommand(command, streams) {
|
|
297
|
+
const manifest = inspectVobsProject(command.options);
|
|
298
|
+
const output = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
299
|
+
if (command.options.out === undefined) {
|
|
300
|
+
streams.stdout.write(output);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
writeInspectOutput(manifest, command.options.out);
|
|
304
|
+
streams.stdout.write(`Wrote vobs inspect manifest to ${path.resolve(process.cwd(), command.options.out)}\n`);
|
|
305
|
+
}
|
|
306
|
+
return Promise.resolve(0);
|
|
307
|
+
}
|
|
308
|
+
async function runAiServeCommand(streams) {
|
|
309
|
+
if (streams.stdin === undefined)
|
|
310
|
+
throw new Error('vobs ai serve requires stdin');
|
|
311
|
+
const input = createInterface({ input: streams.stdin, crlfDelay: Infinity });
|
|
312
|
+
const serverStreams = {
|
|
313
|
+
input,
|
|
314
|
+
stdout: streams.stdout,
|
|
315
|
+
...(streams.stderr === undefined ? {} : { stderr: streams.stderr }),
|
|
316
|
+
};
|
|
317
|
+
try {
|
|
318
|
+
await runVobsAiServer(serverStreams);
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
input.close();
|
|
322
|
+
}
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
88
325
|
function parseNewArgs(args, deprecatedAlias) {
|
|
89
326
|
let type;
|
|
90
327
|
let name;
|
|
@@ -239,6 +476,11 @@ function helpText() {
|
|
|
239
476
|
'Usage:',
|
|
240
477
|
' vobs check [--tsconfig tsconfig.json] [--view view.html]',
|
|
241
478
|
' vobs new [app|app-di|enterprise|lib|lib-di] [name] [--with-di] [--force]',
|
|
479
|
+
' vobs inspect --json [--tsconfig tsconfig.json] [--out manifest.json]',
|
|
480
|
+
' vobs ai tools --json',
|
|
481
|
+
' vobs ai serve JSON-RPC stdio bridge for read-only AI tools',
|
|
482
|
+
' vobs context --json (--route path | --file source) [--depth 0..4]',
|
|
483
|
+
' vobs diagnose --json [--route path | --file source] [--depth 0..4]',
|
|
242
484
|
'',
|
|
243
485
|
'Check options:',
|
|
244
486
|
' -p, --tsconfig <path> TypeScript project file. Defaults to ./tsconfig.json.',
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** @license MIT
|
|
2
|
+
* Copyright (c) 2026 vobsjs
|
|
3
|
+
* @vobs/cli
|
|
4
|
+
*/
|
|
5
|
+
import type { VobsProjectManifest } from '@vobs/ai-contracts';
|
|
6
|
+
export interface InspectOptions {
|
|
7
|
+
readonly tsconfigPath: string;
|
|
8
|
+
readonly out?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function inspectVobsProject(options: InspectOptions): VobsProjectManifest;
|
|
11
|
+
export declare function writeInspectOutput(manifest: VobsProjectManifest, outputPath: string): void;
|