@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.
- package/dist/command.d.ts +44 -0
- package/dist/command.js +45 -0
- package/dist/generators/init.js +6 -3
- package/dist/generators/service.d.ts +16 -0
- package/dist/generators/service.js +164 -0
- package/dist/nx/repository-preset.js +65 -0
- package/dist/product-version.d.ts +2 -0
- package/dist/product-version.js +15 -0
- package/generators/service/files/AGENTS.md.template +31 -0
- package/generators/service/files/habitat.toml.template +10 -0
- package/generators/service/files/package.json.template +26 -0
- package/generators/service/files/src/client.ts.template +35 -0
- package/generators/service/files/src/service/base.ts.template +17 -0
- package/generators/service/files/src/service/contract.ts.template +8 -0
- package/generators/service/files/src/service/impl.ts.template +9 -0
- package/generators/service/files/src/service/modules/__moduleFileName__/AGENTS.md.template +22 -0
- package/generators/service/files/src/service/modules/__moduleFileName__/contract/__operationFileName__.ts.template +15 -0
- package/generators/service/files/src/service/modules/__moduleFileName__/contract/index.ts.template +6 -0
- package/generators/service/files/src/service/modules/__moduleFileName__/module.ts.template +6 -0
- package/generators/service/files/src/service/modules/__moduleFileName__/router/__operationFileName__.ts.template +7 -0
- package/generators/service/files/src/service/modules/__moduleFileName__/router.ts.template +6 -0
- package/generators/service/files/src/service/router.ts.template +5 -0
- package/generators/service/files/tsconfig.build.json.template +7 -0
- package/generators/service/files/tsconfig.json.template +11 -0
- package/generators/service/schema.json +30 -0
- package/generators.json +5 -0
- package/oclif.manifest.json +1 -1
- 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
|
+
}
|
package/dist/command.js
ADDED
|
@@ -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
|
+
}
|
package/dist/generators/init.js
CHANGED
|
@@ -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
|
|
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 (!
|
|
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,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,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 { 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,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,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
|
}
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@habitat-ai/cli",
|
|
3
|
-
"version": "0.5.
|
|
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.
|
|
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",
|