@vobs/cli 0.1.0 → 0.3.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 +31 -49
- 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 +16 -17
- package/package.json +13 -11
|
@@ -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\": \"
|
|
7
|
+
readonly content: "{\n \"name\": \"{{packageName}}\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"type\": \"module\",\n \"engines\": {\n \"node\": \">=22.12.0\"\n },\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\": \"0.1.0\",\n \"@vobs/authorization\": \"0.1.0\",\n \"@vobs/forms\": \"0.1.0\",\n \"@vobs/i18n\": \"0.1.0\",\n \"@vobs/reactivity\": \"0.1.0\",\n \"@vobs/router\": \"0.1.0\",\n \"@vobs/runtime-dom\": \"0.1.0\",\n \"@vobs/server-renderer\": \"0.1.0\",\n \"@vobs/ui\": \"0.2.0\"\n },\n \"devDependencies\": {\n \"@vobs/vite-plugin\": \"0.1.0\",\n \"typescript\": \"^5.7.0\",\n \"vite\": \"^8.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";
|
|
@@ -7,6 +7,9 @@ const packageJson = `{
|
|
|
7
7
|
"version": "0.1.0",
|
|
8
8
|
"private": true,
|
|
9
9
|
"type": "module",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22.12.0"
|
|
12
|
+
},
|
|
10
13
|
"scripts": {
|
|
11
14
|
"dev": "vite",
|
|
12
15
|
"build": "tsc --noEmit && vite build",
|
|
@@ -14,19 +17,20 @@ const packageJson = `{
|
|
|
14
17
|
"typecheck": "tsc --noEmit"
|
|
15
18
|
},
|
|
16
19
|
"dependencies": {
|
|
17
|
-
"@vobs/di": "
|
|
18
|
-
"@vobs/
|
|
19
|
-
"@vobs/
|
|
20
|
-
"@vobs/
|
|
21
|
-
"@vobs/
|
|
22
|
-
"@vobs/
|
|
23
|
-
"@vobs/
|
|
24
|
-
"@vobs/
|
|
20
|
+
"@vobs/di": "0.1.0",
|
|
21
|
+
"@vobs/authorization": "0.1.0",
|
|
22
|
+
"@vobs/forms": "0.1.0",
|
|
23
|
+
"@vobs/i18n": "0.1.0",
|
|
24
|
+
"@vobs/reactivity": "0.1.0",
|
|
25
|
+
"@vobs/router": "0.1.0",
|
|
26
|
+
"@vobs/runtime-dom": "0.1.0",
|
|
27
|
+
"@vobs/server-renderer": "0.1.0",
|
|
28
|
+
"@vobs/ui": "0.2.0"
|
|
25
29
|
},
|
|
26
30
|
"devDependencies": {
|
|
27
|
-
"@vobs/vite-plugin": "
|
|
31
|
+
"@vobs/vite-plugin": "0.1.0",
|
|
28
32
|
"typescript": "^5.7.0",
|
|
29
|
-
"vite": "^
|
|
33
|
+
"vite": "^8.0.0"
|
|
30
34
|
}
|
|
31
35
|
}
|
|
32
36
|
`;
|
|
@@ -131,10 +135,7 @@ export function createEnterpriseI18n() {
|
|
|
131
135
|
}
|
|
132
136
|
`;
|
|
133
137
|
const servicesTs = `import { createInjectionKey, type InjectionScope } from '@vobs/di';
|
|
134
|
-
import {
|
|
135
|
-
AuthorizationPortKey,
|
|
136
|
-
defaultAuthorizationPort,
|
|
137
|
-
} from '../access/authorization.js';
|
|
138
|
+
import { AuthorizationPortKey, authorizer } from '../access/authorization.js';
|
|
138
139
|
|
|
139
140
|
export interface AuditService {
|
|
140
141
|
record(event: string, details: Readonly<Record<string, unknown>>): void;
|
|
@@ -143,7 +144,7 @@ export interface AuditService {
|
|
|
143
144
|
export const AuditServiceKey = createInjectionKey<AuditService>('enterprise.audit');
|
|
144
145
|
|
|
145
146
|
export function registerEnterpriseServices(scope: InjectionScope): void {
|
|
146
|
-
scope.provide(AuthorizationPortKey,
|
|
147
|
+
scope.provide(AuthorizationPortKey, authorizer);
|
|
147
148
|
scope.provide(AuditServiceKey, {
|
|
148
149
|
record(event, details) {
|
|
149
150
|
console.info('[audit]', event, details);
|
|
@@ -151,45 +152,24 @@ export function registerEnterpriseServices(scope: InjectionScope): void {
|
|
|
151
152
|
});
|
|
152
153
|
}
|
|
153
154
|
`;
|
|
154
|
-
const authorizationTs = `import {
|
|
155
|
+
const authorizationTs = `import {
|
|
156
|
+
AuthorizationPortKey,
|
|
157
|
+
createAuthorizer,
|
|
158
|
+
type AuthorizationSubject,
|
|
159
|
+
} from '@vobs/authorization';
|
|
155
160
|
import type { RouteGuardContext, RouteGuardResult } from '@vobs/router';
|
|
156
161
|
|
|
157
|
-
export interface AuthorizationSubject {
|
|
158
|
-
readonly id: string;
|
|
159
|
-
readonly capabilities: readonly string[];
|
|
160
|
-
}
|
|
161
|
-
|
|
162
162
|
export interface AuthorizationRequestContext {
|
|
163
163
|
readonly authorizationSubject?: AuthorizationSubject | null;
|
|
164
164
|
}
|
|
165
165
|
|
|
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
|
-
};
|
|
166
|
+
export const authorizer = createAuthorizer({
|
|
167
|
+
roles: [{ name: 'local-admin', permissions: ['dashboard:view', 'users:write'] }],
|
|
168
|
+
});
|
|
189
169
|
|
|
190
170
|
const developmentSubject: AuthorizationSubject = {
|
|
191
171
|
id: 'local-admin',
|
|
192
|
-
|
|
172
|
+
roles: ['local-admin'],
|
|
193
173
|
};
|
|
194
174
|
|
|
195
175
|
export function requireCapabilities(
|
|
@@ -198,8 +178,10 @@ export function requireCapabilities(
|
|
|
198
178
|
): RouteGuardResult {
|
|
199
179
|
const subject = readAuthorizationSubject(context.request);
|
|
200
180
|
if (subject === null) return '/login';
|
|
201
|
-
const
|
|
202
|
-
|
|
181
|
+
const allowed = requiredCapabilities.every((capability) =>
|
|
182
|
+
authorizer.can(subject, capability, { type: 'route', attributes: { path: context.route.path } }),
|
|
183
|
+
);
|
|
184
|
+
return allowed ? true : '/403';
|
|
203
185
|
}
|
|
204
186
|
|
|
205
187
|
function readAuthorizationSubject(request: unknown): AuthorizationSubject | null {
|
|
@@ -606,8 +588,8 @@ const readme = `# {{projectName}}
|
|
|
606
588
|
## Run
|
|
607
589
|
|
|
608
590
|
~~~bash
|
|
609
|
-
|
|
610
|
-
|
|
591
|
+
pnpm install
|
|
592
|
+
pnpm run dev
|
|
611
593
|
~~~
|
|
612
594
|
|
|
613
595
|
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;
|