@habitat-ai/cli 0.5.4 → 0.5.6

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.
Files changed (28) hide show
  1. package/dist/command.d.ts +44 -0
  2. package/dist/command.js +45 -0
  3. package/dist/generators/init.js +6 -3
  4. package/dist/generators/service.d.ts +16 -0
  5. package/dist/generators/service.js +164 -0
  6. package/dist/nx/repository-preset.js +65 -0
  7. package/dist/product-version.d.ts +2 -0
  8. package/dist/product-version.js +15 -0
  9. package/generators/service/files/AGENTS.md.template +31 -0
  10. package/generators/service/files/habitat.toml.template +10 -0
  11. package/generators/service/files/package.json.template +26 -0
  12. package/generators/service/files/src/client.ts.template +35 -0
  13. package/generators/service/files/src/service/base.ts.template +17 -0
  14. package/generators/service/files/src/service/contract.ts.template +8 -0
  15. package/generators/service/files/src/service/impl.ts.template +9 -0
  16. package/generators/service/files/src/service/modules/__moduleFileName__/AGENTS.md.template +22 -0
  17. package/generators/service/files/src/service/modules/__moduleFileName__/contract/__operationFileName__.ts.template +15 -0
  18. package/generators/service/files/src/service/modules/__moduleFileName__/contract/index.ts.template +6 -0
  19. package/generators/service/files/src/service/modules/__moduleFileName__/module.ts.template +6 -0
  20. package/generators/service/files/src/service/modules/__moduleFileName__/router/__operationFileName__.ts.template +7 -0
  21. package/generators/service/files/src/service/modules/__moduleFileName__/router.ts.template +6 -0
  22. package/generators/service/files/src/service/router.ts.template +5 -0
  23. package/generators/service/files/tsconfig.build.json.template +7 -0
  24. package/generators/service/files/tsconfig.json.template +11 -0
  25. package/generators/service/schema.json +30 -0
  26. package/generators.json +5 -0
  27. package/oclif.manifest.json +1 -1
  28. package/package.json +8 -2
@@ -0,0 +1,44 @@
1
+ import { Command } from "@oclif/core";
2
+ /** Normalized values for the shared machine-output and mutation-control flags. */
3
+ export type HabitatBaseFlags = {
4
+ json: boolean;
5
+ dryRun: boolean;
6
+ yes: boolean;
7
+ };
8
+ /** Structured command failure rendered consistently for machine and human callers. */
9
+ export type HabitatError = {
10
+ message: string;
11
+ code?: string;
12
+ details?: unknown;
13
+ };
14
+ /** Stable success-or-failure envelope returned by Habitat command projections. */
15
+ export type HabitatResult<TData = unknown> = {
16
+ ok: true;
17
+ data?: TData;
18
+ warnings?: string[];
19
+ meta?: Record<string, unknown>;
20
+ } | {
21
+ ok: false;
22
+ error: HabitatError;
23
+ meta?: Record<string, unknown>;
24
+ };
25
+ /** Shared result and output contract for native Oclif command projections. */
26
+ export declare abstract class HabitatCommand extends Command {
27
+ static baseFlags: {
28
+ readonly json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
29
+ readonly "dry-run": import("@oclif/core/interfaces").BooleanFlag<boolean>;
30
+ readonly yes: import("@oclif/core/interfaces").BooleanFlag<boolean>;
31
+ };
32
+ protected ok<TData>(data?: TData, meta?: Record<string, unknown>, warnings?: string[]): HabitatResult<TData>;
33
+ protected fail(message: string, options?: {
34
+ code?: string;
35
+ details?: unknown;
36
+ meta?: Record<string, unknown>;
37
+ }): HabitatResult<never>;
38
+ /** Renders one result through the selected machine or human output channel. */
39
+ protected outputResult<TData>(result: HabitatResult<TData>, options?: {
40
+ flags?: HabitatBaseFlags;
41
+ human?: (result: HabitatResult<TData>) => void;
42
+ }): Promise<void>;
43
+ static extractBaseFlags(flags: Record<string, unknown>): HabitatBaseFlags;
44
+ }
@@ -0,0 +1,45 @@
1
+ import { Command, Flags } from "@oclif/core";
2
+ import { writeJsonResult } from "./lib/output.js";
3
+ /** Shared result and output contract for native Oclif command projections. */
4
+ export class HabitatCommand extends Command {
5
+ static baseFlags = {
6
+ json: Flags.boolean({ description: "Output machine-readable JSON" }),
7
+ "dry-run": Flags.boolean({
8
+ description: "Print actions without making any changes",
9
+ }),
10
+ yes: Flags.boolean({
11
+ char: "y",
12
+ description: "Assume yes for prompts/confirmation",
13
+ }),
14
+ };
15
+ ok(data, meta, warnings) {
16
+ return { ok: true, data, meta, warnings };
17
+ }
18
+ fail(message, options) {
19
+ return {
20
+ ok: false,
21
+ error: { message, code: options?.code, details: options?.details },
22
+ meta: options?.meta,
23
+ };
24
+ }
25
+ /** Renders one result through the selected machine or human output channel. */
26
+ async outputResult(result, options) {
27
+ const flags = options?.flags ?? { json: false, dryRun: false, yes: false };
28
+ if (flags.json) {
29
+ await writeJsonResult(result);
30
+ return;
31
+ }
32
+ if (options?.human) {
33
+ options.human(result);
34
+ return;
35
+ }
36
+ this.log(result.ok ? "ok" : `error: ${result.error.message}`);
37
+ }
38
+ static extractBaseFlags(flags) {
39
+ return {
40
+ json: Boolean(flags.json),
41
+ dryRun: Boolean(flags["dry-run"] ?? flags.dryRun),
42
+ yes: Boolean(flags.yes),
43
+ };
44
+ }
45
+ }
@@ -1,12 +1,15 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { getPackageManagerCommand, installPackagesTask, runTasksInSerial, } from "@nx/devkit";
3
3
  import { initializeHabitatConsumer } from "../nx/initialization.js";
4
- import { assertHabitatBunConsumer } from "../nx/repository-preset.js";
4
+ import { assertHabitatBunConsumer, initializeHabitatBunRepository, } from "../nx/repository-preset.js";
5
5
  import { habitatConsumerBinding } from "../nx-generators.js";
6
6
  /** Initializes the installed Habitat package inside one Nx consumer. */
7
7
  export default function initializeHabitat(tree) {
8
8
  assertHabitatBunConsumer(tree);
9
- const result = initializeHabitatConsumer(tree, habitatConsumerBinding);
9
+ const repository = initializeHabitatBunRepository(tree, habitatConsumerBinding, {
10
+ packageManager: "bun",
11
+ });
12
+ const consumer = initializeHabitatConsumer(tree, habitatConsumerBinding);
10
13
  const activateHusky = () => {
11
14
  const command = `${getPackageManagerCommand("bun").exec} husky`;
12
15
  execSync(command, {
@@ -14,7 +17,7 @@ export default function initializeHabitat(tree) {
14
17
  stdio: "inherit",
15
18
  });
16
19
  };
17
- if (!result.packageChanged)
20
+ if (!repository.packageChanged && !consumer.packageChanged)
18
21
  return activateHusky;
19
22
  return runTasksInSerial(() => installPackagesTask(tree, false, "", "bun"), activateHusky);
20
23
  }
@@ -0,0 +1,16 @@
1
+ import { type GeneratorCallback, type Tree } from "@nx/devkit";
2
+ /** Exact non-SDK dependency pins emitted into every constructed service package. */
3
+ export declare const SERVICE_GENERATOR_DEPENDENCY_VERSIONS: {
4
+ readonly "@orpc/contract": "2.0.0-beta.23";
5
+ readonly "@orpc/server": "2.0.0-beta.23";
6
+ readonly typebox: "1.3.8";
7
+ };
8
+ /** Options for constructing one closed Habitat service package. */
9
+ export interface ServiceGeneratorOptions {
10
+ readonly name: string;
11
+ readonly directory: string;
12
+ readonly module: string;
13
+ readonly operation: string;
14
+ }
15
+ /** Constructs a private service package through Nx and schedules Bun dependency installation. */
16
+ export default function createService(tree: Tree, options: ServiceGeneratorOptions): Promise<GeneratorCallback>;
@@ -0,0 +1,164 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { addProjectConfiguration, generateFiles, getProjects, installPackagesTask, names, OverwriteStrategy, readJson, } from "@nx/devkit";
3
+ import { buildPackageJsonPatterns, buildPackageJsonWorkspacesMatcher, } from "nx/src/plugins/package-json";
4
+ import { Type } from "typebox";
5
+ import { Validator } from "typebox/schema";
6
+ import { assertHabitatBunConsumer } from "../nx/repository-preset.js";
7
+ import { installedSdkVersion } from "../product-version.js";
8
+ const PACKAGE_NAME_PATTERN = "^(?:@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$";
9
+ const KEBAB_NAME_PATTERN = "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$";
10
+ const STRICT_IDENTIFIER_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
11
+ /** Exact non-SDK dependency pins emitted into every constructed service package. */
12
+ export const SERVICE_GENERATOR_DEPENDENCY_VERSIONS = {
13
+ "@orpc/contract": "2.0.0-beta.23",
14
+ "@orpc/server": "2.0.0-beta.23",
15
+ typebox: "1.3.8",
16
+ };
17
+ const RESERVED_IDENTIFIERS = new Set([
18
+ "arguments",
19
+ "await",
20
+ "break",
21
+ "case",
22
+ "catch",
23
+ "class",
24
+ "const",
25
+ "continue",
26
+ "debugger",
27
+ "default",
28
+ "delete",
29
+ "do",
30
+ "else",
31
+ "enum",
32
+ "eval",
33
+ "export",
34
+ "extends",
35
+ "false",
36
+ "finally",
37
+ "for",
38
+ "function",
39
+ "if",
40
+ "implements",
41
+ "import",
42
+ "in",
43
+ "instanceof",
44
+ "interface",
45
+ "let",
46
+ "new",
47
+ "null",
48
+ "package",
49
+ "private",
50
+ "protected",
51
+ "public",
52
+ "return",
53
+ "static",
54
+ "super",
55
+ "switch",
56
+ "this",
57
+ "throw",
58
+ "true",
59
+ "try",
60
+ "typeof",
61
+ "var",
62
+ "void",
63
+ "while",
64
+ "with",
65
+ "yield",
66
+ ]);
67
+ const ORPC_IMPLEMENTER_MEMBERS = new Set(["lazy", "middleware", "router", "use"]);
68
+ const SERVICE_CONTEXT_LANES = new Set(["config", "deps", "invocation", "provided", "scope"]);
69
+ const ServiceGeneratorOptionsSchema = Type.Object({
70
+ name: Type.String({ minLength: 1, maxLength: 214, pattern: PACKAGE_NAME_PATTERN }),
71
+ directory: Type.String({ minLength: 1 }),
72
+ module: Type.String({ pattern: KEBAB_NAME_PATTERN }),
73
+ operation: Type.String({ pattern: KEBAB_NAME_PATTERN }),
74
+ }, { additionalProperties: false });
75
+ const optionsValidator = new Validator({}, ServiceGeneratorOptionsSchema);
76
+ /** Constructs a private service package through Nx and schedules Bun dependency installation. */
77
+ export default async function createService(tree, options) {
78
+ assertOptions(options);
79
+ assertPortableDirectory(options.directory);
80
+ assertHabitatBunConsumer(tree);
81
+ assertWorkspaceDestination(tree, options.directory);
82
+ const moduleNames = names(options.module);
83
+ const operationNames = names(options.operation);
84
+ assertStrictIdentifier("module", moduleNames.propertyName);
85
+ assertStrictIdentifier("operation", operationNames.propertyName);
86
+ assertAvailableModuleName(moduleNames.propertyName);
87
+ const stagedProjects = getProjects(tree);
88
+ if (stagedProjects.has(options.name)) {
89
+ throw new Error(`Cannot generate service '${options.name}': an Nx project already uses that name.`);
90
+ }
91
+ const stagedRootCollision = [...stagedProjects.entries()].find(([, project]) => project.root === options.directory);
92
+ if (stagedRootCollision !== undefined) {
93
+ throw new Error(`Cannot generate service '${options.name}': destination '${options.directory}' is already owned by staged Nx project '${stagedRootCollision[0]}'.`);
94
+ }
95
+ if (tree.exists(options.directory) || tree.children(options.directory).length > 0) {
96
+ throw new Error(`Cannot generate service '${options.name}': destination '${options.directory}' is occupied.`);
97
+ }
98
+ const tsconfigBasePath = `${"../".repeat(options.directory.split("/").length)}tsconfig.base.json`;
99
+ const templateRoot = fileURLToPath(new URL("../../generators/service/files", import.meta.url));
100
+ const dependenciesJson = indentJson({
101
+ "@habitat-ai/sdk": installedSdkVersion(),
102
+ ...SERVICE_GENERATOR_DEPENDENCY_VERSIONS,
103
+ }, 2);
104
+ generateFiles(tree, templateRoot, options.directory, {
105
+ directory: options.directory,
106
+ directoryLiteral: JSON.stringify(options.directory),
107
+ dependenciesJson,
108
+ moduleFileName: moduleNames.fileName,
109
+ moduleName: moduleNames.propertyName,
110
+ moduleLocalName: moduleNames.propertyName,
111
+ name: options.name,
112
+ nameLiteral: JSON.stringify(options.name),
113
+ operationFileName: operationNames.fileName,
114
+ operationName: operationNames.propertyName,
115
+ tsconfigBasePath,
116
+ }, { overwriteStrategy: OverwriteStrategy.ThrowIfExisting });
117
+ addProjectConfiguration(tree, options.name, {
118
+ root: options.directory,
119
+ tags: ["type:service", "role:servicepackage"],
120
+ targets: {
121
+ check: {
122
+ executor: "nx:noop",
123
+ },
124
+ },
125
+ });
126
+ return () => installPackagesTask(tree, true, "", "bun");
127
+ }
128
+ function assertOptions(options) {
129
+ if (!optionsValidator.Check(options)) {
130
+ throw new Error("Service generator options require a valid package name, workspace-relative directory, and kebab-case module and operation.");
131
+ }
132
+ }
133
+ function assertPortableDirectory(directory) {
134
+ const segments = directory.split("/");
135
+ if (directory.startsWith("/") ||
136
+ /^[A-Za-z]:/.test(directory) ||
137
+ directory.includes("\\") ||
138
+ directory.trim() !== directory ||
139
+ segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
140
+ throw new Error(`Cannot generate service in '${directory}': directory must be a normalized workspace-relative path without traversal.`);
141
+ }
142
+ }
143
+ function assertWorkspaceDestination(tree, directory) {
144
+ const patterns = buildPackageJsonPatterns(tree.root, (path) => readJson(tree, path));
145
+ const isWorkspacePackage = buildPackageJsonWorkspacesMatcher(patterns);
146
+ if (!isWorkspacePackage(`${directory}/package.json`)) {
147
+ throw new Error(`Cannot generate service in '${directory}': destination must match a Bun workspace declared in package.json.`);
148
+ }
149
+ }
150
+ function assertStrictIdentifier(kind, identifier) {
151
+ if (!STRICT_IDENTIFIER_PATTERN.test(identifier) ||
152
+ RESERVED_IDENTIFIERS.has(identifier) ||
153
+ ORPC_IMPLEMENTER_MEMBERS.has(identifier)) {
154
+ throw new Error(`Cannot generate service: ${kind} '${identifier}' collides with JavaScript or oRPC implementer syntax.`);
155
+ }
156
+ }
157
+ function assertAvailableModuleName(identifier) {
158
+ if (SERVICE_CONTEXT_LANES.has(identifier)) {
159
+ throw new Error(`Cannot generate service: module '${identifier}' is reserved for the service context.`);
160
+ }
161
+ }
162
+ function indentJson(value, spaces) {
163
+ return JSON.stringify(value, null, 2).replaceAll("\n", `\n${" ".repeat(spaces)}`);
164
+ }
@@ -15,7 +15,9 @@ const ALTERNATE_PACKAGE_MANAGER_PATHS = [
15
15
  ];
16
16
  const BUN_VERSION = "1.3.14";
17
17
  const BIOME_VERSION = "2.5.3";
18
+ const ESLINT_VERSION = "10.0.3";
18
19
  const NX_VERSION = "23.1.1";
20
+ const TYPESCRIPT_ESLINT_PARSER_VERSION = "8.66.0";
19
21
  const RootPackageSchema = Type.Object({
20
22
  private: Type.Optional(Type.Boolean({ description: "Whether the repository root is excluded from publication." })),
21
23
  type: Type.Optional(Type.String({ description: "Module system selected by the repository root." })),
@@ -101,8 +103,12 @@ const standardScripts = {
101
103
  };
102
104
  const standardDevDependencies = {
103
105
  "@biomejs/biome": BIOME_VERSION,
106
+ "@nx/eslint": NX_VERSION,
107
+ "@nx/eslint-plugin": NX_VERSION,
108
+ "@typescript-eslint/parser": TYPESCRIPT_ESLINT_PARSER_VERSION,
104
109
  "@types/node": "24.13.3",
105
110
  "bun-types": BUN_VERSION,
111
+ eslint: ESLINT_VERSION,
106
112
  nx: NX_VERSION,
107
113
  typescript: "5.9.3",
108
114
  };
@@ -121,6 +127,10 @@ const nativeNxPresetNamedInputs = {
121
127
  default: ["{projectRoot}/**/*", "sharedGlobals"],
122
128
  production: ["default"],
123
129
  };
130
+ const nxEslintPlugin = {
131
+ plugin: "@nx/eslint/plugin",
132
+ options: { targetName: "check:boundaries" },
133
+ };
124
134
  const standardTargetDefaults = {
125
135
  build: {
126
136
  cache: true,
@@ -132,6 +142,7 @@ const standardTargetDefaults = {
132
142
  cache: false,
133
143
  dependsOn: [
134
144
  { projects: ["habitat"], target: "lint" },
145
+ "check:boundaries",
135
146
  "typecheck",
136
147
  "verify",
137
148
  "check:policy",
@@ -164,6 +175,45 @@ linker = "isolated"
164
175
  # Registry versions remain registry consumers; source relationships opt in with workspace:*.
165
176
  linkWorkspacePackages = false
166
177
  `;
178
+ const eslintConfig = `import nxPlugin from "@nx/eslint-plugin";
179
+ import tsParser from "@typescript-eslint/parser";
180
+
181
+ export default [
182
+ ...nxPlugin.configs["flat/base"],
183
+ {
184
+ files: ["**/*.{js,jsx,cjs,mjs,ts,tsx,cts,mts}"],
185
+ ignores: [
186
+ "**/node_modules/**",
187
+ "**/dist/**",
188
+ "**/coverage/**",
189
+ "**/.nx/**",
190
+ "**/.habitat/cache/**",
191
+ "**/.tmp/**",
192
+ ],
193
+ rules: {
194
+ "@nx/enforce-module-boundaries": [
195
+ "error",
196
+ {
197
+ allow: [],
198
+ depConstraints: [],
199
+ enforceBuildableLibDependency: false,
200
+ },
201
+ ],
202
+ },
203
+ },
204
+ {
205
+ files: ["**/*.{ts,tsx,cts,mts}"],
206
+ languageOptions: {
207
+ parser: tsParser,
208
+ parserOptions: {
209
+ ecmaFeatures: { jsx: true },
210
+ ecmaVersion: "latest",
211
+ sourceType: "module",
212
+ },
213
+ },
214
+ },
215
+ ];
216
+ `;
167
217
  const standardTypeScriptCompilerOptions = {
168
218
  target: "ES2022",
169
219
  module: "ESNext",
@@ -465,12 +515,26 @@ function planRootPackage(packageJson) {
465
515
  function planNxJson(nxJson) {
466
516
  assertReservedNxValues("named input", nxJson.namedInputs, standardNamedInputs, nativeNxPresetNamedInputs);
467
517
  assertReservedNxValues("target default", nxJson.targetDefaults, standardTargetDefaults);
518
+ const plugins = planNxEslintRegistration(nxJson.plugins);
468
519
  return {
469
520
  ...nxJson,
521
+ plugins,
470
522
  namedInputs: { ...nxJson.namedInputs, ...standardNamedInputs },
471
523
  targetDefaults: { ...nxJson.targetDefaults, ...standardTargetDefaults },
472
524
  };
473
525
  }
526
+ function planNxEslintRegistration(plugins) {
527
+ const existing = plugins ?? [];
528
+ const matches = existing.filter((plugin) => (typeof plugin === "string" ? plugin : plugin.plugin) === nxEslintPlugin.plugin);
529
+ if (matches.length > 1) {
530
+ throw new Error("nx.json contains multiple Nx ESLint plugin registrations.");
531
+ }
532
+ const match = matches[0];
533
+ if (match !== undefined && !isDeepStrictEqual(match, nxEslintPlugin)) {
534
+ throw new Error("nx.json contains an incompatible Nx ESLint plugin registration.");
535
+ }
536
+ return match === undefined ? [...existing, nxEslintPlugin] : [...existing];
537
+ }
474
538
  function assertReservedNxValues(kind, existing, canonical, admittedPredecessors = {}) {
475
539
  for (const [name, value] of Object.entries(canonical)) {
476
540
  const current = existing?.[name];
@@ -505,6 +569,7 @@ function plannedTextFiles(tree) {
505
569
  return [
506
570
  { path: "biome.json", contents: biomeConfig },
507
571
  { path: "bunfig.toml", contents: bunfig },
572
+ { path: "eslint.config.mjs", contents: eslintConfig },
508
573
  { path: HABITAT_PROJECT_PATH, contents: habitatProject },
509
574
  ].filter((file) => !tree.exists(file.path) || isEmptyTextFile(tree, file.path));
510
575
  }
@@ -0,0 +1,2 @@
1
+ /** Returns the exact SDK version paired with the installed Habitat CLI. */
2
+ export declare function installedSdkVersion(): string;
@@ -0,0 +1,15 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { fileURLToPath } from "node:url";
3
+ const EXACT_SEMVER_PATTERN = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
4
+ const cliPackageJsonPath = fileURLToPath(new URL("../package.json", import.meta.url));
5
+ /** Returns the exact SDK version paired with the installed Habitat CLI. */
6
+ export function installedSdkVersion() {
7
+ const manifest = JSON.parse(readFileSync(cliPackageJsonPath, "utf8"));
8
+ const version = manifest.dependencies?.["@habitat-ai/sdk"];
9
+ if (typeof version !== "string" ||
10
+ !EXACT_SEMVER_PATTERN.test(version) ||
11
+ version !== manifest.version) {
12
+ throw new Error("Installed @habitat-ai/cli and @habitat-ai/sdk must use one identical exact semantic version.");
13
+ }
14
+ return version;
15
+ }
@@ -0,0 +1,31 @@
1
+ # <%= name %> Service Router
2
+
3
+ ## Purpose
4
+
5
+ - Own the runtime capability exposed through this package's public client.
6
+
7
+ ## Scope
8
+
9
+ - Applies to this service package and its private implementation spine.
10
+
11
+ ## Boundaries
12
+
13
+ - Consumers enter only through `src/client.ts` and the package's `./client` export.
14
+ - App composition supplies ready resources and public service clients through context.
15
+ - Transports, orchestration, provider acquisition, and runtime mounting stay outside this service.
16
+
17
+ ## Flow
18
+
19
+ - The public contract descends through one native oRPC implementation lineage.
20
+ - The module projects its named dependency into the direct handler vocabulary, and its router leaf
21
+ authors `<%= operationName %>`.
22
+ - Native oRPC retains inherited context additively; the direct projection narrows authorship rather
23
+ than claiming runtime erasure.
24
+ - Module and service routers only compose already-authored operations.
25
+ - The initial operation uses an inline native `.handler(...)`. When authored operations genuinely
26
+ require Effect, install the official `.effect(...)` extension once in `src/service/impl.ts` and
27
+ author their generators directly in the operation routers.
28
+
29
+ ## Validation
30
+
31
+ - Run `bunx nx run <%= name %>:typecheck` and `bunx nx run <%= name %>:build`.
@@ -0,0 +1,10 @@
1
+ schemaVersion = 1
2
+ id = <%- nameLiteral %>
3
+ ownerProject = <%- nameLiteral %>
4
+ blueprint = "service"
5
+ blueprintVersion = 1
6
+
7
+ [roots]
8
+ project = <%- directoryLiteral %>
9
+
10
+ [selections]
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "<%= name %>",
3
+ "private": true,
4
+ "type": "module",
5
+ "packageManager": "bun@1.3.14",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "exports": {
10
+ "./client": {
11
+ "types": "./dist/client.d.ts",
12
+ "default": "./dist/client.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "bun --eval 'await import(\"node:fs/promises\").then(({ rm }) => rm(\"dist\", { recursive: true, force: true }))' && tsc -p tsconfig.build.json",
17
+ "typecheck": "tsc -p tsconfig.json --noEmit"
18
+ },
19
+ "dependencies": <%- dependenciesJson %>,
20
+ "nx": {
21
+ "tags": [
22
+ "type:service",
23
+ "role:servicepackage"
24
+ ]
25
+ }
26
+ }
@@ -0,0 +1,35 @@
1
+ import type { RouterContractClient } from "@orpc/contract";
2
+ import { createRouterClient } from "@orpc/server";
3
+ import type { Context } from "./service/base.js";
4
+ import { type Contract, contract } from "./service/contract.js";
5
+ import { router } from "./service/router.js";
6
+
7
+ export { type Contract, contract };
8
+
9
+ /** Host-supplied ready capabilities used by service operations. */
10
+ export type Deps = Context["deps"];
11
+
12
+ /** Stable caller scope fixed at client construction. */
13
+ export type Scope = Context["scope"];
14
+
15
+ /** App-selected service policy fixed at client construction. */
16
+ export type Config = Context["config"];
17
+
18
+ /** Public construction boundary for one in-process client. */
19
+ export type CreateClientOptions = Pick<Context, "deps" | "scope" | "config">;
20
+
21
+ /** Typed caller surface derived from the public contract. */
22
+ export type Client = RouterContractClient<Contract>;
23
+
24
+ /** Constructs the sole public client over the private service router. */
25
+ export function createClient({ deps, scope, config }: CreateClientOptions): Client {
26
+ return createRouterClient(router, {
27
+ context: {
28
+ deps,
29
+ scope,
30
+ config,
31
+ invocation: {},
32
+ provided: {},
33
+ } satisfies Context,
34
+ });
35
+ }
@@ -0,0 +1,17 @@
1
+ import { os } from "@orpc/server";
2
+
3
+ type EmptyContextLane = Readonly<Record<PropertyKey, never>>;
4
+
5
+ /** Complete five-lane context supplied before module-local curation. */
6
+ export type Context = {
7
+ readonly deps: {
8
+ readonly <%= moduleName %>: unknown;
9
+ };
10
+ readonly scope: EmptyContextLane;
11
+ readonly config: EmptyContextLane;
12
+ readonly invocation: EmptyContextLane;
13
+ readonly provided: EmptyContextLane;
14
+ };
15
+
16
+ /** Standalone middleware author rooted in the complete service context. */
17
+ export const base = os.$context<Context>();
@@ -0,0 +1,8 @@
1
+ import { oc } from "@orpc/contract";
2
+ import { contract as <%= moduleLocalName %> } from "./modules/<%= moduleFileName %>/contract/index.js";
3
+
4
+ /** Public service contract composed from its semantic modules. */
5
+ export const contract = oc.router({ <%= moduleName %>: <%= moduleLocalName %> });
6
+
7
+ /** Caller contract type exposed through the public client face. */
8
+ export type Contract = typeof contract;
@@ -0,0 +1,9 @@
1
+ import { implement } from "@orpc/server";
2
+ import type { Context } from "./base.js";
3
+ import { contract } from "./contract.js";
4
+
5
+ /** Unconfigured implementer retained for aggregate router completion. */
6
+ export const impl = implement(contract).$context<Context>();
7
+
8
+ /** Configured service lineage from which modules descend. */
9
+ export const service = impl;
@@ -0,0 +1,22 @@
1
+ # <%= moduleName %> Module Router
2
+
3
+ ## Purpose
4
+
5
+ - Own the `<%= operationName %>` operation at this service boundary.
6
+
7
+ ## Boundaries
8
+
9
+ - The contract directory owns caller-visible schemas.
10
+ - `module.ts` projects the named dependency into the direct handler vocabulary without acquiring
11
+ resources or providers.
12
+ - Native oRPC retains inherited context additively; the projection narrows authorship rather than
13
+ claiming runtime erasure.
14
+ - The named router leaf owns operation behavior; router faces only compose.
15
+
16
+ ## Interfaces
17
+
18
+ - `<%= operationName %>` is the module's native operation.
19
+
20
+ ## Validation
21
+
22
+ - Run the owning service's typecheck and build targets.
@@ -0,0 +1,15 @@
1
+ import { standard } from "@habitat-ai/sdk/service/schema";
2
+ import { oc } from "@orpc/contract";
3
+ import { Type } from "typebox";
4
+
5
+ const InputSchema = Type.Object(
6
+ {},
7
+ { additionalProperties: false, description: "Empty <%= operationName %> operation input." }
8
+ );
9
+ const OutputSchema = Type.Object(
10
+ {},
11
+ { additionalProperties: false, description: "Empty <%= operationName %> operation output." }
12
+ );
13
+
14
+ /** Minimal TypeBox-backed <%= operationName %> operation contract. */
15
+ export const <%= operationName %> = oc.input(standard(InputSchema)).output(standard(OutputSchema));
@@ -0,0 +1,6 @@
1
+ import { <%= operationName %> } from "./<%= operationFileName %>.js";
2
+
3
+ /** Module contract composed from its semantic operation leaves. */
4
+ export const contract = {
5
+ <%= operationName %>,
6
+ };
@@ -0,0 +1,6 @@
1
+ import { service } from "../../impl.js";
2
+
3
+ /** Projects the module's named dependency into the direct handler vocabulary. */
4
+ export const module = service.<%= moduleName %>.use(({ context, next }) =>
5
+ next({ context: { <%= moduleName %>: context.deps.<%= moduleName %> } })
6
+ );
@@ -0,0 +1,7 @@
1
+ import { module } from "../module.js";
2
+
3
+ /** Authors the <%= operationName %> operation at its named router leaf. */
4
+ export const <%= operationName %> = module.<%= operationName %>.handler(({ context }) => {
5
+ void context.<%= moduleName %>;
6
+ return {};
7
+ });
@@ -0,0 +1,6 @@
1
+ import { <%= operationName %> } from "./router/<%= operationFileName %>.js";
2
+
3
+ /** Composes completed operations into the module router face. */
4
+ export const router = {
5
+ <%= operationName %>,
6
+ };
@@ -0,0 +1,5 @@
1
+ import { impl } from "./impl.js";
2
+ import { router as <%= moduleLocalName %> } from "./modules/<%= moduleFileName %>/router.js";
3
+
4
+ /** Completes the contract from already-authored module routers. */
5
+ export const router = impl.router({ <%= moduleName %>: <%= moduleLocalName %> });
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "noEmit": false
6
+ }
7
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "extends": "<%= tsconfigBasePath %>",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src",
6
+ "noUnusedLocals": true
7
+ },
8
+ "include": [
9
+ "src"
10
+ ]
11
+ }
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "name": {
6
+ "type": "string",
7
+ "minLength": 1,
8
+ "maxLength": 214,
9
+ "pattern": "^(?:@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$",
10
+ "description": "Exact Nx project, package, Habitat instance, and owner identity."
11
+ },
12
+ "directory": {
13
+ "type": "string",
14
+ "minLength": 1,
15
+ "description": "Full workspace-relative root for the service package."
16
+ },
17
+ "module": {
18
+ "type": "string",
19
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
20
+ "description": "Kebab-case semantic module name."
21
+ },
22
+ "operation": {
23
+ "type": "string",
24
+ "pattern": "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$",
25
+ "description": "Kebab-case semantic operation name."
26
+ }
27
+ },
28
+ "required": ["name", "directory", "module", "operation"],
29
+ "additionalProperties": false
30
+ }
package/generators.json CHANGED
@@ -15,6 +15,11 @@
15
15
  "factory": "./dist/generators/remove-hook.js",
16
16
  "schema": "./generators.schema.json",
17
17
  "description": "Remove Habitat's named Codex hook contribution."
18
+ },
19
+ "service": {
20
+ "factory": "./dist/generators/service.js",
21
+ "schema": "./generators/service/schema.json",
22
+ "description": "Construct one closed Habitat service package."
18
23
  }
19
24
  }
20
25
  }
@@ -99,5 +99,5 @@
99
99
  ]
100
100
  }
101
101
  },
102
- "version": "0.5.4"
102
+ "version": "0.5.6"
103
103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@habitat-ai/cli",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,6 +18,7 @@
18
18
  "files": [
19
19
  "bin",
20
20
  "dist",
21
+ "generators",
21
22
  "generators.json",
22
23
  "generators.schema.json",
23
24
  "preset.schema.json",
@@ -35,6 +36,11 @@
35
36
  ]
36
37
  },
37
38
  "exports": {
39
+ "./command": {
40
+ "types": "./dist/command.d.ts",
41
+ "import": "./dist/command.js",
42
+ "default": "./dist/command.js"
43
+ },
38
44
  "./nx-plugin": {
39
45
  "types": "./dist/nx-plugin.d.ts",
40
46
  "import": "./dist/nx-plugin.js",
@@ -52,7 +58,7 @@
52
58
  "test": "vitest run --project habitat-cli"
53
59
  },
54
60
  "dependencies": {
55
- "@habitat-ai/sdk": "0.5.4",
61
+ "@habitat-ai/sdk": "0.5.6",
56
62
  "@nx/devkit": "23.1.1",
57
63
  "@oclif/core": "^4.13.2",
58
64
  "@oclif/plugin-help": "^6.2.27",