@habitat-ai/cli 0.5.2 → 0.5.4

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/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # `@habitat-ai/cli`
2
+
3
+ Habitat's Oclif CLI and native Nx integration. The package owns the portable
4
+ repository preset, Habitat policy projection, and the public `habitat` command.
5
+
6
+ ## Create A Repository
7
+
8
+ Nx invokes the package's Bun-only `preset` generator while creating the workspace:
9
+
10
+ ```sh
11
+ bunx create-nx-workspace@23.1.1 my-workspace \
12
+ --preset=@habitat-ai/cli \
13
+ --packageManager=bun
14
+ ```
15
+
16
+ Nx initializes Git after the preset returns. Activate the repository hooks once
17
+ that boundary exists:
18
+
19
+ ```sh
20
+ cd my-workspace
21
+ bunx nx generate @habitat-ai/cli:init --no-interactive
22
+ ```
23
+
24
+ The preset creates generic Bun, Nx, TypeScript, Biome, and Habitat scheduler
25
+ configuration. It does not copy blueprints, select product policy, or author the
26
+ repository's `AGENTS.md` hierarchy.
27
+
28
+ ## Adopt An Existing Bun Nx Repository
29
+
30
+ ```sh
31
+ bunx nx add @habitat-ai/cli --no-interactive
32
+ ```
33
+
34
+ `nx add` invokes `init`. Existing nonempty hooks and unrelated Nx/Codex
35
+ configuration remain consumer-owned. A Bun repository may additionally invoke the
36
+ `preset` generator to adopt the portable repository spine.
37
+
38
+ Later package releases use ordinary Nx migrations:
39
+
40
+ ```sh
41
+ bunx nx migrate @habitat-ai/cli@latest
42
+ ```
@@ -1,16 +1,14 @@
1
1
  import { execSync } from "node:child_process";
2
- import { detectPackageManager, getPackageManagerCommand, installPackagesTask, runTasksInSerial, } from "@nx/devkit";
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
5
  import { habitatConsumerBinding } from "../nx-generators.js";
5
6
  /** Initializes the installed Habitat package inside one Nx consumer. */
6
7
  export default function initializeHabitat(tree) {
7
- const packageManager = detectPackageManager(tree.root);
8
- if (packageManager === "yarn") {
9
- throw new Error("Habitat Husky initialization supports npm, pnpm, and Bun consumers.");
10
- }
8
+ assertHabitatBunConsumer(tree);
11
9
  const result = initializeHabitatConsumer(tree, habitatConsumerBinding);
12
10
  const activateHusky = () => {
13
- const command = `${getPackageManagerCommand(packageManager).exec} husky`;
11
+ const command = `${getPackageManagerCommand("bun").exec} husky`;
14
12
  execSync(command, {
15
13
  cwd: tree.root,
16
14
  stdio: "inherit",
@@ -18,5 +16,5 @@ export default function initializeHabitat(tree) {
18
16
  };
19
17
  if (!result.packageChanged)
20
18
  return activateHusky;
21
- return runTasksInSerial(() => installPackagesTask(tree), activateHusky);
19
+ return runTasksInSerial(() => installPackagesTask(tree, false, "", "bun"), activateHusky);
22
20
  }
@@ -0,0 +1,4 @@
1
+ import { type GeneratorCallback, type Tree } from "@nx/devkit";
2
+ import { type HabitatRepositoryPresetOptions } from "../nx/repository-preset.js";
3
+ /** Creates Habitat's portable Bun/Nx repository spine before Git initialization. */
4
+ export default function createHabitatRepository(tree: Tree, options: HabitatRepositoryPresetOptions): GeneratorCallback | undefined;
@@ -0,0 +1,10 @@
1
+ import { installPackagesTask } from "@nx/devkit";
2
+ import { initializeHabitatBunRepository, } from "../nx/repository-preset.js";
3
+ import { habitatConsumerBinding } from "../nx-generators.js";
4
+ /** Creates Habitat's portable Bun/Nx repository spine before Git initialization. */
5
+ export default function createHabitatRepository(tree, options) {
6
+ const result = initializeHabitatBunRepository(tree, habitatConsumerBinding, options);
7
+ if (!result.packageChanged)
8
+ return undefined;
9
+ return () => installPackagesTask(tree, false, "", "bun");
10
+ }
@@ -1,4 +1,4 @@
1
- import { type PluginConfiguration, type Tree } from "@nx/devkit";
1
+ import { type NxJsonConfiguration, type PluginConfiguration, type Tree } from "@nx/devkit";
2
2
  import { type Static, Type } from "typebox";
3
3
  declare const HookGroupSchema: Type.TObject<{
4
4
  _habitat: Type.TOptional<Type.TObject<{
@@ -21,6 +21,10 @@ declare const HabitatOwnedHookGroupSchema: Type.TObject<{
21
21
  }>;
22
22
  type HookGroup = Static<typeof HookGroupSchema>;
23
23
  type HabitatOwnedHookGroup = Static<typeof HabitatOwnedHookGroupSchema>;
24
+ type PlannedValue<T> = Readonly<{
25
+ changed: boolean;
26
+ value: T;
27
+ }>;
24
28
  /** App-owned identities and exact predecessor states consumed by native Nx initialization. */
25
29
  export type HabitatConsumerBinding = Readonly<{
26
30
  defaultCheckScript: string;
@@ -53,4 +57,6 @@ export type HabitatInitializationResult = Readonly<{
53
57
  export declare function initializeHabitatConsumer(tree: Tree, binding: HabitatConsumerBinding): HabitatInitializationResult;
54
58
  /** Removes only Habitat's named hook group while preserving installed Nx integration. */
55
59
  export declare function removeHabitatHook(tree: Tree, binding: HabitatConsumerBinding): void;
60
+ /** Plans the one package-owned Nx plugin registration without mutating a Tree. */
61
+ export declare function planHabitatNxRegistration(nxJson: NxJsonConfiguration, binding: HabitatConsumerBinding): PlannedValue<NxJsonConfiguration>;
56
62
  export {};
@@ -82,7 +82,7 @@ export function initializeHabitatConsumer(tree, binding) {
82
82
  const nxJson = requireNxJson(tree);
83
83
  const hooks = readHooks(tree);
84
84
  const packageJson = readPackage(tree);
85
- const nxPlan = planNxInitialization(nxJson, binding);
85
+ const nxPlan = planHabitatNxRegistration(nxJson, binding);
86
86
  const hookPlan = planHookInitialization(hooks, binding);
87
87
  const packagePlan = planPackageInitialization(packageJson, binding);
88
88
  const gitHookPlan = planGitHookInitialization(tree, binding.gitHook);
@@ -129,7 +129,8 @@ function readPackage(tree) {
129
129
  }
130
130
  return input;
131
131
  }
132
- function planNxInitialization(nxJson, binding) {
132
+ /** Plans the one package-owned Nx plugin registration without mutating a Tree. */
133
+ export function planHabitatNxRegistration(nxJson, binding) {
133
134
  const plugins = nxJson.plugins ?? [];
134
135
  const matches = plugins.filter((plugin) => hasPluginIdentity(plugin, binding.nxPlugin) ||
135
136
  binding.predecessorNxPlugins.some((predecessor) => hasPluginIdentity(plugin, typeof predecessor === "string" ? predecessor : predecessor.plugin)));
@@ -0,0 +1,22 @@
1
+ import { type Tree } from "@nx/devkit";
2
+ import { type HabitatConsumerBinding } from "./initialization.js";
3
+ /** Nx options consumed by the portable Habitat repository preset. */
4
+ export type HabitatRepositoryPresetOptions = Readonly<{
5
+ packageManager: string;
6
+ }>;
7
+ /** Reports whether dependency installation must follow repository generation. */
8
+ export type HabitatRepositoryPresetResult = Readonly<{
9
+ packageChanged: boolean;
10
+ }>;
11
+ /**
12
+ * Admits package-owned initialization only for the canonical Bun consumer shape.
13
+ * The check runs before the generator plans package, Nx, hook, or policy changes.
14
+ */
15
+ export declare function assertHabitatBunConsumer(tree: Tree): void;
16
+ /**
17
+ * Converges one repository on Habitat's portable Bun/Nx configuration.
18
+ *
19
+ * Product inventory, policy selections, documentation routers, aliases, release
20
+ * policy, and hosted CI remain consumer-owned and are never generated here.
21
+ */
22
+ export declare function initializeHabitatBunRepository(tree: Tree, binding: HabitatConsumerBinding, options: HabitatRepositoryPresetOptions): HabitatRepositoryPresetResult;
@@ -0,0 +1,530 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { getProjects, readJson, readNxJson, updateNxJson, writeJson, } from "@nx/devkit";
3
+ import { Type } from "typebox";
4
+ import { Validator } from "typebox/schema";
5
+ import { planHabitatNxRegistration } from "./initialization.js";
6
+ const PACKAGE_PATH = "package.json";
7
+ const HABITAT_PROJECT_PATH = "scripts/habitat/project.json";
8
+ const TYPESCRIPT_CONFIG_PATH = "tsconfig.base.json";
9
+ const ALTERNATE_PACKAGE_MANAGER_PATHS = [
10
+ "package-lock.json",
11
+ "npm-shrinkwrap.json",
12
+ "pnpm-lock.yaml",
13
+ "pnpm-workspace.yaml",
14
+ "yarn.lock",
15
+ ];
16
+ const BUN_VERSION = "1.3.14";
17
+ const BIOME_VERSION = "2.5.3";
18
+ const NX_VERSION = "23.1.1";
19
+ const RootPackageSchema = Type.Object({
20
+ private: Type.Optional(Type.Boolean({ description: "Whether the repository root is excluded from publication." })),
21
+ type: Type.Optional(Type.String({ description: "Module system selected by the repository root." })),
22
+ packageManager: Type.Optional(Type.String({ description: "Package manager and version selected by the repository root." })),
23
+ workspaces: Type.Optional(Type.Array(Type.String(), { description: "Bun workspace roots owned by the repository." })),
24
+ scripts: Type.Optional(Type.Record(Type.String(), Type.String(), {
25
+ description: "Root command faces delegated to the Nx scheduler.",
26
+ })),
27
+ devDependencies: Type.Optional(Type.Record(Type.String(), Type.String(), {
28
+ description: "Development tools installed at the repository root.",
29
+ })),
30
+ dependencies: Type.Optional(Type.Record(Type.String(), Type.String(), {
31
+ description: "Runtime dependencies installed at the repository root.",
32
+ })),
33
+ optionalDependencies: Type.Optional(Type.Record(Type.String(), Type.String(), {
34
+ description: "Optional dependencies installed at the repository root.",
35
+ })),
36
+ peerDependencies: Type.Optional(Type.Record(Type.String(), Type.String(), {
37
+ description: "Peer dependencies declared by the repository root.",
38
+ })),
39
+ overrides: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
40
+ description: "Root dependency versions overridden during Bun resolution.",
41
+ })),
42
+ resolutions: Type.Optional(Type.Record(Type.String(), Type.Unknown(), {
43
+ description: "Root dependency versions selected during Bun resolution.",
44
+ })),
45
+ patchedDependencies: Type.Optional(Type.Record(Type.String(), Type.String(), {
46
+ description: "Dependency patches applied by Bun during installation.",
47
+ })),
48
+ nx: Type.Optional(Type.Object({
49
+ includedScripts: Type.Optional(Type.Array(Type.String(), {
50
+ description: "Root scripts intentionally projected into the Nx project graph.",
51
+ })),
52
+ }, { additionalProperties: true, description: "Nx root package projection controls." })),
53
+ }, { additionalProperties: true, description: "Bun Nx repository package metadata." });
54
+ const TypeScriptConfigSchema = Type.Object({
55
+ compilerOptions: Type.Optional(Type.Object({
56
+ types: Type.Optional(Type.Array(Type.String(), {
57
+ description: "Ambient type packages loaded by the repository compiler.",
58
+ })),
59
+ }, {
60
+ additionalProperties: true,
61
+ description: "Compiler foundation and consumer-specific TypeScript options.",
62
+ })),
63
+ }, { additionalProperties: true });
64
+ const HabitatProjectSchema = Type.Object({
65
+ name: Type.Literal("habitat", {
66
+ description: "Canonical Nx identity for repository-wide Habitat policy tasks.",
67
+ }),
68
+ root: Type.Literal("scripts/habitat", {
69
+ description: "Canonical owner root for repository-wide Habitat policy tasks.",
70
+ }),
71
+ tags: Type.Tuple([Type.Literal("type:tool"), Type.Literal("role:architecture-policy")], {
72
+ description: "Canonical Nx classification for the Habitat policy owner.",
73
+ }),
74
+ targets: Type.Record(Type.String(), Type.Unknown(), {
75
+ description: "Nx tasks owned by the repository-wide Habitat policy project.",
76
+ }),
77
+ }, { additionalProperties: true });
78
+ const packageValidator = new Validator({}, RootPackageSchema);
79
+ const habitatProjectValidator = new Validator({}, HabitatProjectSchema);
80
+ const typescriptConfigValidator = new Validator({}, TypeScriptConfigSchema);
81
+ const standardWorkspaces = [
82
+ "apps/*",
83
+ "services/*",
84
+ "packages/*",
85
+ "resources/*",
86
+ "plugins/cli/topics/*",
87
+ "plugins/web/*",
88
+ "plugins/server/api/*",
89
+ "plugins/async/workflows/*",
90
+ "plugins/async/schedules/*",
91
+ ];
92
+ const standardScripts = {
93
+ build: "nx run-many -t build",
94
+ check: "nx run-many -t check",
95
+ ci: "nx run-many -t build,check,test",
96
+ "ci:affected": "nx affected -t build,check,test",
97
+ format: "nx run habitat:format",
98
+ lint: "nx run habitat:lint",
99
+ test: "nx run-many -t test",
100
+ typecheck: "nx run-many -t typecheck",
101
+ };
102
+ const standardDevDependencies = {
103
+ "@biomejs/biome": BIOME_VERSION,
104
+ "@types/node": "24.13.3",
105
+ "bun-types": BUN_VERSION,
106
+ nx: NX_VERSION,
107
+ typescript: "5.9.3",
108
+ };
109
+ const standardNamedInputs = {
110
+ default: ["{projectRoot}/**/*", "!{projectRoot}/dist/**", "!{projectRoot}/coverage/**"],
111
+ production: [
112
+ "default",
113
+ "!{projectRoot}/test/**",
114
+ "!{projectRoot}/**/*.test.*",
115
+ "!{projectRoot}/**/*.spec.*",
116
+ ],
117
+ bunToolchain: ["{workspaceRoot}/package.json", "{workspaceRoot}/bun.lock"],
118
+ typescriptRuntime: ["{workspaceRoot}/tsconfig.base.json", "{workspaceRoot}/bun.lock"],
119
+ };
120
+ const nativeNxPresetNamedInputs = {
121
+ default: ["{projectRoot}/**/*", "sharedGlobals"],
122
+ production: ["default"],
123
+ };
124
+ const standardTargetDefaults = {
125
+ build: {
126
+ cache: true,
127
+ dependsOn: ["^build"],
128
+ inputs: ["production", "^production", "typescriptRuntime"],
129
+ outputs: ["{projectRoot}/dist"],
130
+ },
131
+ check: {
132
+ cache: false,
133
+ dependsOn: [
134
+ { projects: ["habitat"], target: "lint" },
135
+ "typecheck",
136
+ "verify",
137
+ "check:policy",
138
+ "^check",
139
+ ],
140
+ outputs: [],
141
+ },
142
+ test: {
143
+ cache: true,
144
+ dependsOn: ["^build"],
145
+ inputs: ["default", "^default", "typescriptRuntime"],
146
+ outputs: [],
147
+ },
148
+ typecheck: {
149
+ cache: true,
150
+ dependsOn: ["^build"],
151
+ inputs: ["default", "^default", "typescriptRuntime"],
152
+ outputs: [],
153
+ },
154
+ verify: {
155
+ cache: false,
156
+ dependsOn: ["build", "^build"],
157
+ outputs: [],
158
+ },
159
+ };
160
+ const bunfig = `env = false
161
+
162
+ [install]
163
+ linker = "isolated"
164
+ # Registry versions remain registry consumers; source relationships opt in with workspace:*.
165
+ linkWorkspacePackages = false
166
+ `;
167
+ const standardTypeScriptCompilerOptions = {
168
+ target: "ES2022",
169
+ module: "ESNext",
170
+ moduleResolution: "Bundler",
171
+ lib: ["ES2022"],
172
+ strict: true,
173
+ skipLibCheck: true,
174
+ types: ["bun-types", "node"],
175
+ };
176
+ /**
177
+ * Admits package-owned initialization only for the canonical Bun consumer shape.
178
+ * The check runs before the generator plans package, Nx, hook, or policy changes.
179
+ */
180
+ export function assertHabitatBunConsumer(tree) {
181
+ assertBunRepositoryArtifacts(tree);
182
+ const packageJson = readRootPackage(tree);
183
+ const requiredManager = `bun@${BUN_VERSION}`;
184
+ if (packageJson.packageManager !== requiredManager) {
185
+ throw new Error(`Habitat initialization requires packageManager '${requiredManager}'; received '${packageJson.packageManager}'.`);
186
+ }
187
+ const nxPackageManager = requireNxJson(tree).cli?.packageManager;
188
+ if (nxPackageManager !== undefined && nxPackageManager !== "bun") {
189
+ throw new Error(`Habitat initialization requires Nx package manager 'bun'; received '${nxPackageManager}'.`);
190
+ }
191
+ }
192
+ const biomeConfig = jsonDocument({
193
+ $schema: `https://biomejs.dev/schemas/${BIOME_VERSION}/schema.json`,
194
+ vcs: {
195
+ enabled: true,
196
+ clientKind: "git",
197
+ useIgnoreFile: true,
198
+ },
199
+ files: {
200
+ ignoreUnknown: true,
201
+ includes: [
202
+ "**",
203
+ "!**/node_modules/**",
204
+ "!**/dist/**",
205
+ "!**/coverage/**",
206
+ "!.nx/**",
207
+ "!.habitat/cache/**",
208
+ "!**/.tmp/**",
209
+ ],
210
+ },
211
+ formatter: {
212
+ enabled: true,
213
+ useEditorconfig: true,
214
+ formatWithErrors: false,
215
+ indentStyle: "space",
216
+ indentWidth: 2,
217
+ lineWidth: 100,
218
+ lineEnding: "lf",
219
+ },
220
+ linter: {
221
+ enabled: true,
222
+ rules: {
223
+ preset: "none",
224
+ correctness: {
225
+ noConstAssign: "error",
226
+ noSelfAssign: "error",
227
+ noUnreachable: "error",
228
+ noUnreachableSuper: "error",
229
+ noUnsafeFinally: "error",
230
+ noUnsafeOptionalChaining: "error",
231
+ useIsNan: "error",
232
+ useValidTypeof: "error",
233
+ },
234
+ suspicious: {
235
+ noDebugger: "error",
236
+ noDuplicateCase: "error",
237
+ noDuplicateClassMembers: "error",
238
+ noDuplicateJsxProps: "error",
239
+ noDuplicateObjectKeys: "error",
240
+ noDuplicateParameters: "error",
241
+ noFallthroughSwitchClause: "error",
242
+ noGlobalIsFinite: "error",
243
+ noGlobalIsNan: "error",
244
+ noSparseArray: "error",
245
+ noWith: "error",
246
+ useGetterReturn: "error",
247
+ },
248
+ },
249
+ },
250
+ javascript: {
251
+ formatter: {
252
+ quoteStyle: "double",
253
+ semicolons: "always",
254
+ trailingCommas: "es5",
255
+ },
256
+ },
257
+ assist: {
258
+ enabled: true,
259
+ actions: {
260
+ source: {
261
+ organizeImports: "on",
262
+ },
263
+ },
264
+ },
265
+ });
266
+ const habitatProjectDocument = {
267
+ $schema: "../../node_modules/nx/schemas/project-schema.json",
268
+ name: "habitat",
269
+ root: "scripts/habitat",
270
+ tags: ["type:tool", "role:architecture-policy"],
271
+ targets: {
272
+ lint: {
273
+ executor: "nx:run-commands",
274
+ cache: true,
275
+ inputs: [
276
+ "{workspaceRoot}/**/*.{js,mjs,jsx,cjs,ts,mts,cts,tsx,json,jsonld,webapp,webmanifest,jsonc,code-snippets,code-workspace,sublime-build,sublime-commands,sublime-completions,sublime-keymap,sublime-macro,sublime-menu,sublime-mousemap,sublime-project,sublime-settings,sublime-theme,sublime-workspace,sublime_metrics,sublime_session,css,graphqls,graphql,gql,html,svg,astro,vue,svelte,grit}",
277
+ "{workspaceRoot}/**/{.all-contributorsrc,.arcconfig,.auto-changelog,.bowerrc,.c8rc,.htmlhintrc,.imgbotconfig,.jslintrc,.nycrc,.tern-config,.tern-project,.vuerc,.watchmanconfig,.ember-cli,.jscsrc,.jshintrc,.babelrc,.hintrc,.swcrc,mcmod.info}",
278
+ "{workspaceRoot}/.editorconfig",
279
+ "{workspaceRoot}/.gitignore",
280
+ "{workspaceRoot}/biome.json",
281
+ "bunToolchain",
282
+ ],
283
+ outputs: [],
284
+ options: {
285
+ command: "biome lint --diagnostic-level=error .",
286
+ },
287
+ },
288
+ format: {
289
+ executor: "nx:run-commands",
290
+ cache: false,
291
+ options: {
292
+ command: "biome format --write .",
293
+ },
294
+ },
295
+ check: {
296
+ executor: "nx:noop",
297
+ cache: false,
298
+ outputs: [],
299
+ },
300
+ },
301
+ };
302
+ const habitatProject = jsonDocument(habitatProjectDocument);
303
+ /**
304
+ * Converges one repository on Habitat's portable Bun/Nx configuration.
305
+ *
306
+ * Product inventory, policy selections, documentation routers, aliases, release
307
+ * policy, and hosted CI remain consumer-owned and are never generated here.
308
+ */
309
+ export function initializeHabitatBunRepository(tree, binding, options) {
310
+ assertBunManager(options.packageManager);
311
+ assertBunRepositoryArtifacts(tree);
312
+ const packageJson = readRootPackage(tree);
313
+ assertBunPackage(packageJson);
314
+ assertCanonicalTextFoundation(tree);
315
+ assertHabitatProjectAuthority(tree);
316
+ const typescriptConfig = readTypeScriptConfig(tree);
317
+ const nxJson = requireNxJson(tree);
318
+ const nxRegistration = planHabitatNxRegistration(nxJson, binding);
319
+ const nextPackage = planRootPackage(packageJson);
320
+ const nextNx = planNxJson(nxRegistration.value);
321
+ const nextTypeScriptConfig = planTypeScriptConfig(typescriptConfig);
322
+ const files = plannedTextFiles(tree);
323
+ if (!isDeepStrictEqual(packageJson, nextPackage))
324
+ writeJson(tree, PACKAGE_PATH, nextPackage);
325
+ if (!isDeepStrictEqual(nxJson, nextNx))
326
+ updateNxJson(tree, nextNx);
327
+ if (!isDeepStrictEqual(typescriptConfig, nextTypeScriptConfig)) {
328
+ writeJson(tree, TYPESCRIPT_CONFIG_PATH, nextTypeScriptConfig);
329
+ }
330
+ for (const file of files)
331
+ tree.write(file.path, file.contents);
332
+ return { packageChanged: !isDeepStrictEqual(packageJson, nextPackage) };
333
+ }
334
+ function readRootPackage(tree) {
335
+ if (!tree.exists(PACKAGE_PATH)) {
336
+ throw new Error("Habitat repository generation requires package.json.");
337
+ }
338
+ const input = readJson(tree, PACKAGE_PATH);
339
+ if (!packageValidator.Check(input)) {
340
+ throw new Error("package.json is not a supported Bun Nx repository document.");
341
+ }
342
+ return input;
343
+ }
344
+ function requireNxJson(tree) {
345
+ const nxJson = readNxJson(tree);
346
+ if (nxJson === null) {
347
+ throw new Error("Habitat repository generation requires nx.json.");
348
+ }
349
+ return nxJson;
350
+ }
351
+ function readTypeScriptConfig(tree) {
352
+ if (!tree.exists(TYPESCRIPT_CONFIG_PATH) || isEmptyTextFile(tree, TYPESCRIPT_CONFIG_PATH)) {
353
+ return undefined;
354
+ }
355
+ const input = readJson(tree, TYPESCRIPT_CONFIG_PATH);
356
+ if (!typescriptConfigValidator.Check(input)) {
357
+ throw new Error(`${TYPESCRIPT_CONFIG_PATH} is not a supported TypeScript configuration.`);
358
+ }
359
+ return input;
360
+ }
361
+ function assertBunManager(packageManager) {
362
+ if (packageManager !== "bun") {
363
+ throw new Error(`Habitat repository preset requires Bun; received '${packageManager}'.`);
364
+ }
365
+ }
366
+ function assertBunRepositoryArtifacts(tree) {
367
+ const alternate = ALTERNATE_PACKAGE_MANAGER_PATHS.find((path) => tree.exists(path));
368
+ if (alternate !== undefined) {
369
+ throw new Error(`Habitat repository preset refuses alternate package-manager artifact '${alternate}'.`);
370
+ }
371
+ }
372
+ function assertCanonicalTextFoundation(tree) {
373
+ for (const [path, canonical] of [
374
+ ["biome.json", biomeConfig],
375
+ ["bunfig.toml", bunfig],
376
+ ]) {
377
+ if (!tree.exists(path) || isEmptyTextFile(tree, path))
378
+ continue;
379
+ if (tree.read(path, "utf8") !== canonical) {
380
+ throw new Error(`Habitat repository preset found incompatible foundation file '${path}'.`);
381
+ }
382
+ }
383
+ }
384
+ function assertBunPackage(packageJson) {
385
+ const requiredManager = `bun@${BUN_VERSION}`;
386
+ if (packageJson.packageManager !== undefined && packageJson.packageManager !== requiredManager) {
387
+ throw new Error(`Habitat repository preset requires packageManager '${requiredManager}'; received '${packageJson.packageManager}'.`);
388
+ }
389
+ if (packageJson.private === false) {
390
+ throw new Error("Habitat repository preset requires a private workspace root.");
391
+ }
392
+ if (packageJson.type !== undefined && packageJson.type !== "module") {
393
+ throw new Error("Habitat repository preset requires an ESM workspace root.");
394
+ }
395
+ const conflictingScript = packageJson.nx?.includedScripts?.find((script) => Object.hasOwn(standardScripts, script));
396
+ if (conflictingScript !== undefined) {
397
+ throw new Error(`Habitat repository preset requires nx.includedScripts to exclude scheduler script '${conflictingScript}'.`);
398
+ }
399
+ for (const [name, command] of Object.entries(standardScripts)) {
400
+ const existing = packageJson.scripts?.[name];
401
+ if (existing !== undefined && existing !== command) {
402
+ throw new Error(`Habitat repository preset found incompatible root scheduler script '${name}'.`);
403
+ }
404
+ }
405
+ for (const [name, version] of Object.entries(standardDevDependencies)) {
406
+ for (const bucket of ["dependencies", "optionalDependencies", "peerDependencies"]) {
407
+ if (packageJson[bucket]?.[name] !== undefined) {
408
+ throw new Error(`Habitat repository preset requires tool dependency '${name}' in devDependencies.`);
409
+ }
410
+ }
411
+ const existing = packageJson.devDependencies?.[name];
412
+ if (existing !== undefined && existing !== version) {
413
+ throw new Error(`Habitat repository preset found incompatible tool dependency '${name}@${existing}'.`);
414
+ }
415
+ for (const control of ["overrides", "resolutions"]) {
416
+ const selected = packageJson[control]?.[name];
417
+ if (selected !== undefined && selected !== version) {
418
+ throw new Error(`Habitat repository preset found incompatible ${control} selection for tool '${name}'.`);
419
+ }
420
+ }
421
+ const patched = Object.keys(packageJson.patchedDependencies ?? {}).find((specifier) => specifier === name || specifier.startsWith(`${name}@`));
422
+ if (patched !== undefined) {
423
+ throw new Error(`Habitat repository preset refuses patched foundation tool '${patched}'.`);
424
+ }
425
+ }
426
+ }
427
+ function assertHabitatProjectAuthority(tree) {
428
+ const emptyHabitatProject = isEmptyTextFile(tree, HABITAT_PROJECT_PATH);
429
+ const projects = getProjects(emptyHabitatProject ? withoutHabitatProject(tree) : tree);
430
+ const project = projects.get("habitat");
431
+ if (project !== undefined && project.root !== "scripts/habitat") {
432
+ throw new Error(`Habitat repository preset found project 'habitat' at incompatible root '${project.root}'.`);
433
+ }
434
+ const conflictingRootOwner = [...projects.entries()].find(([name, configuration]) => name !== "habitat" && configuration.root === "scripts/habitat");
435
+ if (conflictingRootOwner !== undefined) {
436
+ throw new Error(`Habitat repository preset found root 'scripts/habitat' owned by incompatible project '${conflictingRootOwner[0]}'.`);
437
+ }
438
+ if (!tree.exists(HABITAT_PROJECT_PATH) || emptyHabitatProject)
439
+ return;
440
+ const input = readJson(tree, HABITAT_PROJECT_PATH);
441
+ if (!habitatProjectValidator.Check(input)) {
442
+ throw new Error(`${HABITAT_PROJECT_PATH} is not a compatible Habitat policy project.`);
443
+ }
444
+ if (Object.hasOwn(input, "projectType")) {
445
+ throw new Error(`${HABITAT_PROJECT_PATH} must not declare projectType.`);
446
+ }
447
+ for (const target of ["check", "format", "lint"]) {
448
+ if (!isDeepStrictEqual(input.targets[target], habitatProjectDocument.targets[target])) {
449
+ throw new Error(`${HABITAT_PROJECT_PATH} has incompatible Habitat target '${target}'.`);
450
+ }
451
+ }
452
+ }
453
+ function planRootPackage(packageJson) {
454
+ return {
455
+ ...packageJson,
456
+ private: true,
457
+ type: "module",
458
+ packageManager: packageJson.packageManager ?? `bun@${BUN_VERSION}`,
459
+ workspaces: [...new Set([...standardWorkspaces, ...(packageJson.workspaces ?? [])])],
460
+ scripts: { ...standardScripts, ...packageJson.scripts },
461
+ nx: { ...packageJson.nx, includedScripts: [...(packageJson.nx?.includedScripts ?? [])] },
462
+ devDependencies: { ...packageJson.devDependencies, ...standardDevDependencies },
463
+ };
464
+ }
465
+ function planNxJson(nxJson) {
466
+ assertReservedNxValues("named input", nxJson.namedInputs, standardNamedInputs, nativeNxPresetNamedInputs);
467
+ assertReservedNxValues("target default", nxJson.targetDefaults, standardTargetDefaults);
468
+ return {
469
+ ...nxJson,
470
+ namedInputs: { ...nxJson.namedInputs, ...standardNamedInputs },
471
+ targetDefaults: { ...nxJson.targetDefaults, ...standardTargetDefaults },
472
+ };
473
+ }
474
+ function assertReservedNxValues(kind, existing, canonical, admittedPredecessors = {}) {
475
+ for (const [name, value] of Object.entries(canonical)) {
476
+ const current = existing?.[name];
477
+ const predecessor = admittedPredecessors[name];
478
+ if (current !== undefined &&
479
+ !isDeepStrictEqual(current, value) &&
480
+ !isDeepStrictEqual(current, predecessor)) {
481
+ throw new Error(`Habitat repository preset found incompatible Nx ${kind} '${name}'.`);
482
+ }
483
+ }
484
+ }
485
+ function planTypeScriptConfig(config) {
486
+ const existing = (config?.compilerOptions ?? {});
487
+ for (const [name, value] of Object.entries(standardTypeScriptCompilerOptions)) {
488
+ if (name === "types")
489
+ continue;
490
+ const current = existing[name];
491
+ if (current !== undefined && !isDeepStrictEqual(current, value)) {
492
+ throw new Error(`Habitat repository preset found incompatible TypeScript compiler option '${name}'.`);
493
+ }
494
+ }
495
+ return {
496
+ ...config,
497
+ compilerOptions: {
498
+ ...standardTypeScriptCompilerOptions,
499
+ ...existing,
500
+ types: [...new Set([...standardTypeScriptCompilerOptions.types, ...(existing.types ?? [])])],
501
+ },
502
+ };
503
+ }
504
+ function plannedTextFiles(tree) {
505
+ return [
506
+ { path: "biome.json", contents: biomeConfig },
507
+ { path: "bunfig.toml", contents: bunfig },
508
+ { path: HABITAT_PROJECT_PATH, contents: habitatProject },
509
+ ].filter((file) => !tree.exists(file.path) || isEmptyTextFile(tree, file.path));
510
+ }
511
+ function isEmptyTextFile(tree, path) {
512
+ return tree.exists(path) && tree.read(path, "utf8")?.trim().length === 0;
513
+ }
514
+ function withoutHabitatProject(tree) {
515
+ return new Proxy(tree, {
516
+ get(target, property, receiver) {
517
+ if (property === "listChanges") {
518
+ return () => [
519
+ ...target.listChanges().filter((change) => change.path !== HABITAT_PROJECT_PATH),
520
+ { path: HABITAT_PROJECT_PATH, type: "DELETE", content: null },
521
+ ];
522
+ }
523
+ const value = Reflect.get(target, property, receiver);
524
+ return typeof value === "function" ? value.bind(target) : value;
525
+ },
526
+ });
527
+ }
528
+ function jsonDocument(value) {
529
+ return `${JSON.stringify(value, null, 2)}\n`;
530
+ }
package/generators.json CHANGED
@@ -1,6 +1,11 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/schema",
3
3
  "generators": {
4
+ "preset": {
5
+ "factory": "./dist/generators/preset.js",
6
+ "schema": "./preset.schema.json",
7
+ "description": "Create the portable Habitat Bun/Nx repository spine."
8
+ },
4
9
  "init": {
5
10
  "factory": "./dist/generators/init.js",
6
11
  "schema": "./generators.schema.json",
@@ -99,5 +99,5 @@
99
99
  ]
100
100
  }
101
101
  },
102
- "version": "0.5.2"
102
+ "version": "0.5.4"
103
103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@habitat-ai/cli",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -20,6 +20,8 @@
20
20
  "dist",
21
21
  "generators.json",
22
22
  "generators.schema.json",
23
+ "preset.schema.json",
24
+ "README.md",
23
25
  "oclif.manifest.json",
24
26
  "package.json"
25
27
  ],
@@ -27,6 +29,11 @@
27
29
  "habitat": "./bin/run.js"
28
30
  },
29
31
  "generators": "./generators.json",
32
+ "nx-migrations": {
33
+ "packageGroup": [
34
+ "@habitat-ai/sdk"
35
+ ]
36
+ },
30
37
  "exports": {
31
38
  "./nx-plugin": {
32
39
  "types": "./dist/nx-plugin.d.ts",
@@ -45,8 +52,8 @@
45
52
  "test": "vitest run --project habitat-cli"
46
53
  },
47
54
  "dependencies": {
48
- "@habitat-ai/sdk": "0.5.2",
49
- "@nx/devkit": "23.1.0",
55
+ "@habitat-ai/sdk": "0.5.4",
56
+ "@nx/devkit": "23.1.1",
50
57
  "@oclif/core": "^4.13.2",
51
58
  "@oclif/plugin-help": "^6.2.27",
52
59
  "typebox": "1.3.8"
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "packageManager": {
6
+ "type": "string",
7
+ "enum": ["bun"],
8
+ "description": "Package manager selected by create-nx-workspace; Habitat accepts Bun only."
9
+ }
10
+ },
11
+ "required": ["packageManager"],
12
+ "additionalProperties": true
13
+ }