@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.
@@ -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\nnpm install\nnpm 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";
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 {\n AuthorizationPortKey,\n defaultAuthorizationPort,\n} 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, defaultAuthorizationPort);\n scope.provide(AuditServiceKey, {\n record(event, details) {\n console.info('[audit]', event, details);\n },\n });\n}\n";
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 { createInjectionKey } from '@vobs/di';\nimport type { RouteGuardContext, RouteGuardResult } from '@vobs/router';\n\nexport interface AuthorizationSubject {\n readonly id: string;\n readonly capabilities: readonly string[];\n}\n\nexport interface AuthorizationRequestContext {\n readonly authorizationSubject?: AuthorizationSubject | null;\n}\n\nexport interface AuthorizationDecision {\n readonly allowed: boolean;\n readonly missingCapabilities: readonly string[];\n}\n\nexport interface AuthorizationPort {\n evaluate(\n subject: AuthorizationSubject | null,\n requiredCapabilities: readonly string[],\n ): AuthorizationDecision;\n}\n\nexport const AuthorizationPortKey =\n createInjectionKey<AuthorizationPort>('enterprise.authorization');\n\nexport const defaultAuthorizationPort: AuthorizationPort = {\n evaluate(subject, requiredCapabilities) {\n const missingCapabilities = requiredCapabilities.filter(\n (capability) => subject?.capabilities.includes(capability) !== true,\n );\n return { allowed: subject !== null && missingCapabilities.length === 0, missingCapabilities };\n },\n};\n\nconst developmentSubject: AuthorizationSubject = {\n id: 'local-admin',\n capabilities: ['dashboard:view', 'users:write'],\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 decision = defaultAuthorizationPort.evaluate(subject, requiredCapabilities);\n return decision.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";
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, defaultAuthorizationPort);
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 { createInjectionKey } from '@vobs/di';
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 interface AuthorizationDecision {
167
- readonly allowed: boolean;
168
- readonly missingCapabilities: readonly string[];
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
- capabilities: ['dashboard:view', 'users:write'],
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 decision = defaultAuthorizationPort.evaluate(subject, requiredCapabilities);
202
- return decision.allowed ? true : '/403';
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
- npm install
610
- npm run dev
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 = { stdout: process.stdout, stderr: process.stderr }) {
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: args.filter((arg) => arg !== '--no-logo'),
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;