@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.
@@ -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 \"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\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";
@@ -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": "latest",
18
- "@vobs/forms": "latest",
19
- "@vobs/i18n": "latest",
20
- "@vobs/reactivity": "latest",
21
- "@vobs/router": "latest",
22
- "@vobs/runtime-dom": "latest",
23
- "@vobs/server-renderer": "latest",
24
- "@vobs/ui": "latest"
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": "latest",
31
+ "@vobs/vite-plugin": "0.1.0",
28
32
  "typescript": "^5.7.0",
29
- "vite": "^7.0.0"
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, defaultAuthorizationPort);
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 { createInjectionKey } from '@vobs/di';
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 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
- };
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
- capabilities: ['dashboard:view', 'users:write'],
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 decision = defaultAuthorizationPort.evaluate(subject, requiredCapabilities);
202
- return decision.allowed ? true : '/403';
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
- npm install
610
- npm run dev
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 = { 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;