@habitat-ai/cli 0.4.0 → 0.4.2

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.
@@ -1,3 +1,3 @@
1
1
  import { type GeneratorCallback, type Tree } from "@nx/devkit";
2
2
  /** Initializes the installed Habitat package inside one Nx consumer. */
3
- export default function initializeHabitat(tree: Tree): GeneratorCallback | void;
3
+ export default function initializeHabitat(tree: Tree): GeneratorCallback;
@@ -1,10 +1,22 @@
1
- import { installPackagesTask } from "@nx/devkit";
1
+ import { execSync } from "node:child_process";
2
+ import { detectPackageManager, getPackageManagerCommand, installPackagesTask, runTasksInSerial, } from "@nx/devkit";
2
3
  import { initializeHabitatConsumer } from "../nx/initialization.js";
3
4
  import { habitatConsumerBinding } from "../nx-generators.js";
4
5
  /** Initializes the installed Habitat package inside one Nx consumer. */
5
6
  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
+ }
6
11
  const result = initializeHabitatConsumer(tree, habitatConsumerBinding);
12
+ const activateHusky = () => {
13
+ const command = `${getPackageManagerCommand(packageManager).exec} husky`;
14
+ execSync(command, {
15
+ cwd: tree.root,
16
+ stdio: "inherit",
17
+ });
18
+ };
7
19
  if (!result.packageChanged)
8
- return;
9
- return () => installPackagesTask(tree);
20
+ return activateHusky;
21
+ return runTasksInSerial(() => installPackagesTask(tree), activateHusky);
10
22
  }
@@ -23,18 +23,29 @@ type HookGroup = Static<typeof HookGroupSchema>;
23
23
  type HabitatOwnedHookGroup = Static<typeof HabitatOwnedHookGroupSchema>;
24
24
  /** App-owned identities and exact predecessor states consumed by native Nx initialization. */
25
25
  export type HabitatConsumerBinding = Readonly<{
26
+ defaultCheckScript: string;
27
+ gitHook: Readonly<{
28
+ contents: string;
29
+ path: string;
30
+ }>;
26
31
  gritPackage: string;
27
32
  hook: HabitatOwnedHookGroup;
33
+ husky: Readonly<{
34
+ package: string;
35
+ prepare: string;
36
+ predecessorPrepareScripts: readonly string[];
37
+ version: string;
38
+ }>;
28
39
  nxPlugin: string;
29
40
  predecessorHooks: readonly HookGroup[];
30
41
  predecessorNxPlugins: readonly PluginConfiguration[];
31
42
  }>;
32
- /** Observable generator decision needed to schedule the consumer package manager once. */
43
+ /** Reports whether dependency installation must precede local Husky activation. */
33
44
  export type HabitatInitializationResult = Readonly<{
34
45
  packageChanged: boolean;
35
46
  }>;
36
47
  /**
37
- * Converges one Nx consumer on the app-owned Habitat plugin, hook, and Grit trust.
48
+ * Converges one Nx consumer on Habitat's app-owned Nx, Codex, package, and Git-hook state.
38
49
  *
39
50
  * Every admission and compatibility decision completes before the first Tree
40
51
  * write, so an incompatible consumer remains byte-for-byte unchanged.
@@ -51,6 +51,21 @@ const CodexHooksSchema = Type.Object({
51
51
  })),
52
52
  }, { additionalProperties: true, description: "Consumer-owned Codex hook configuration." });
53
53
  const ConsumerPackageSchema = Type.Object({
54
+ dependencies: Type.Optional(Type.Record(Type.String(), Type.String(), {
55
+ description: "Runtime dependencies installed directly in the Nx consumer.",
56
+ })),
57
+ scripts: Type.Optional(Type.Record(Type.String(), Type.String(), {
58
+ description: "Consumer-owned package lifecycle and command scripts.",
59
+ })),
60
+ devDependencies: Type.Optional(Type.Record(Type.String(), Type.String(), {
61
+ description: "Development tools installed directly in the Nx consumer.",
62
+ })),
63
+ optionalDependencies: Type.Optional(Type.Record(Type.String(), Type.String(), {
64
+ description: "Optional runtime dependencies installed in the Nx consumer.",
65
+ })),
66
+ peerDependencies: Type.Optional(Type.Record(Type.String(), Type.String(), {
67
+ description: "Peer dependency contracts declared by the Nx consumer.",
68
+ })),
54
69
  trustedDependencies: Type.Optional(Type.Array(Type.String({ minLength: 1 }), {
55
70
  description: "Package lifecycle scripts explicitly trusted by the Bun consumer.",
56
71
  })),
@@ -58,7 +73,7 @@ const ConsumerPackageSchema = Type.Object({
58
73
  const hooksValidator = new Validator({}, CodexHooksSchema);
59
74
  const packageValidator = new Validator({}, ConsumerPackageSchema);
60
75
  /**
61
- * Converges one Nx consumer on the app-owned Habitat plugin, hook, and Grit trust.
76
+ * Converges one Nx consumer on Habitat's app-owned Nx, Codex, package, and Git-hook state.
62
77
  *
63
78
  * Every admission and compatibility decision completes before the first Tree
64
79
  * write, so an incompatible consumer remains byte-for-byte unchanged.
@@ -69,13 +84,16 @@ export function initializeHabitatConsumer(tree, binding) {
69
84
  const packageJson = readPackage(tree);
70
85
  const nxPlan = planNxInitialization(nxJson, binding);
71
86
  const hookPlan = planHookInitialization(hooks, binding);
72
- const packagePlan = planPackageInitialization(packageJson, binding.gritPackage);
87
+ const packagePlan = planPackageInitialization(packageJson, binding);
88
+ const gitHookPlan = planGitHookInitialization(tree, binding.gitHook);
73
89
  if (nxPlan.changed)
74
90
  updateNxJson(tree, nxPlan.value);
75
91
  if (hookPlan.changed)
76
92
  writeJson(tree, CODEX_HOOKS_PATH, hookPlan.value);
77
93
  if (packagePlan.changed)
78
94
  writeJson(tree, PACKAGE_PATH, packagePlan.value);
95
+ if (gitHookPlan.changed)
96
+ tree.write(binding.gitHook.path, gitHookPlan.value);
79
97
  return { packageChanged: packagePlan.changed };
80
98
  }
81
99
  /** Removes only Habitat's named hook group while preserving installed Nx integration. */
@@ -167,19 +185,64 @@ function planHookRemoval(hooks, binding) {
167
185
  },
168
186
  };
169
187
  }
170
- function planPackageInitialization(packageJson, gritPackage) {
188
+ function planPackageInitialization(packageJson, binding) {
189
+ const { gritPackage, husky } = binding;
171
190
  const trusted = packageJson.trustedDependencies ?? [];
172
191
  const matches = trusted.filter((dependency) => dependency === gritPackage);
173
192
  if (matches.length > 1) {
174
193
  throw new Error(`package.json contains duplicate ${gritPackage} trust entries.`);
175
194
  }
176
- if (matches.length === 1)
195
+ const currentHuskyVersion = packageJson.devDependencies?.[husky.package];
196
+ const invalidHuskyPlacement = ["dependencies", "optionalDependencies", "peerDependencies"].find((bucket) => packageJson[bucket]?.[husky.package] !== undefined);
197
+ if (invalidHuskyPlacement !== undefined) {
198
+ throw new Error(`package.json must declare ${husky.package} only in devDependencies; found ${invalidHuskyPlacement}.`);
199
+ }
200
+ if (currentHuskyVersion !== undefined && currentHuskyVersion !== husky.version) {
201
+ throw new Error(`package.json contains an incompatible ${husky.package} version: ${currentHuskyVersion}.`);
202
+ }
203
+ const currentPrepare = packageJson.scripts?.prepare;
204
+ if (currentPrepare !== undefined &&
205
+ currentPrepare !== husky.prepare &&
206
+ !husky.predecessorPrepareScripts.includes(currentPrepare)) {
207
+ throw new Error("package.json contains an incompatible prepare script.");
208
+ }
209
+ const trustedChanged = matches.length === 0;
210
+ const huskyChanged = currentHuskyVersion === undefined;
211
+ const prepareChanged = currentPrepare !== husky.prepare;
212
+ const currentCheck = packageJson.scripts?.check;
213
+ if (currentCheck !== undefined && currentCheck.trim().length === 0) {
214
+ throw new Error("package.json contains an empty check script.");
215
+ }
216
+ const checkChanged = currentCheck === undefined;
217
+ if (!trustedChanged && !huskyChanged && !prepareChanged && !checkChanged) {
177
218
  return { changed: false, value: packageJson };
219
+ }
178
220
  return {
179
221
  changed: true,
180
- value: { ...packageJson, trustedDependencies: [...trusted, gritPackage] },
222
+ value: {
223
+ ...packageJson,
224
+ scripts: {
225
+ ...packageJson.scripts,
226
+ check: currentCheck ?? binding.defaultCheckScript,
227
+ prepare: husky.prepare,
228
+ },
229
+ devDependencies: {
230
+ ...packageJson.devDependencies,
231
+ [husky.package]: husky.version,
232
+ },
233
+ trustedDependencies: trustedChanged ? [...trusted, gritPackage] : trusted,
234
+ },
181
235
  };
182
236
  }
237
+ function planGitHookInitialization(tree, hook) {
238
+ if (!tree.exists(hook.path))
239
+ return { changed: true, value: hook.contents };
240
+ const contents = tree.read(hook.path, "utf8");
241
+ if (contents === null || contents.trim().length === 0) {
242
+ throw new Error(`${hook.path} is empty.`);
243
+ }
244
+ return { changed: false, value: contents };
245
+ }
183
246
  function oneOwnedHookLocation(hooks, binding) {
184
247
  const identity = binding.hook._habitat.identity;
185
248
  const locations = Object.entries(hooks.hooks ?? {}).flatMap(([event, groups]) => groups.flatMap((group, index) => {
@@ -1,6 +1,17 @@
1
1
  /** Exact app-owned identities projected by the public Habitat Nx generators. */
2
2
  export declare const habitatConsumerBinding: {
3
+ readonly defaultCheckScript: "nx run-many -t check";
4
+ readonly gitHook: {
5
+ readonly path: ".husky/pre-push";
6
+ readonly contents: "# Nested Git work must discover its own repository.\nunset $(git rev-parse --local-env-vars)\nbun run check\n";
7
+ };
3
8
  readonly gritPackage: "@getgrit/cli";
9
+ readonly husky: {
10
+ readonly package: "husky";
11
+ readonly version: "9.1.7";
12
+ readonly prepare: "husky";
13
+ readonly predecessorPrepareScripts: readonly ["./scripts/dev/install-repository-hooks.sh", "git config core.hooksPath scripts/githooks"];
14
+ };
4
15
  readonly nxPlugin: "@habitat-ai/cli/nx-plugin";
5
16
  readonly predecessorNxPlugins: readonly [{
6
17
  readonly plugin: "@habitat/cli/nx-plugin";
@@ -1,6 +1,23 @@
1
1
  /** Exact app-owned identities projected by the public Habitat Nx generators. */
2
2
  export const habitatConsumerBinding = {
3
+ defaultCheckScript: "nx run-many -t check",
4
+ gitHook: {
5
+ path: ".husky/pre-push",
6
+ contents: `# Nested Git work must discover its own repository.
7
+ unset $(git rev-parse --local-env-vars)
8
+ bun run check
9
+ `,
10
+ },
3
11
  gritPackage: "@getgrit/cli",
12
+ husky: {
13
+ package: "husky",
14
+ version: "9.1.7",
15
+ prepare: "husky",
16
+ predecessorPrepareScripts: [
17
+ "./scripts/dev/install-repository-hooks.sh",
18
+ "git config core.hooksPath scripts/githooks",
19
+ ],
20
+ },
4
21
  nxPlugin: "@habitat-ai/cli/nx-plugin",
5
22
  predecessorNxPlugins: [
6
23
  {
@@ -1,34 +1,5 @@
1
1
  {
2
2
  "commands": {
3
- "hook": {
4
- "aliases": [],
5
- "args": {
6
- "name": {
7
- "description": "Named Habitat hook operation",
8
- "name": "name",
9
- "options": [
10
- "agent-stop"
11
- ],
12
- "required": true
13
- }
14
- },
15
- "description": "Run a Habitat local-hook entrypoint",
16
- "flags": {},
17
- "hasDynamicHelp": false,
18
- "hiddenAliases": [],
19
- "id": "hook",
20
- "pluginAlias": "@habitat-ai/cli",
21
- "pluginName": "@habitat-ai/cli",
22
- "pluginType": "core",
23
- "strict": true,
24
- "enableJsonFlag": false,
25
- "isESM": true,
26
- "relativePath": [
27
- "dist",
28
- "commands",
29
- "hook.js"
30
- ]
31
- },
32
3
  "resolve": {
33
4
  "aliases": [],
34
5
  "args": {},
@@ -97,7 +68,36 @@
97
68
  "commands",
98
69
  "check.js"
99
70
  ]
71
+ },
72
+ "hook": {
73
+ "aliases": [],
74
+ "args": {
75
+ "name": {
76
+ "description": "Named Habitat hook operation",
77
+ "name": "name",
78
+ "options": [
79
+ "agent-stop"
80
+ ],
81
+ "required": true
82
+ }
83
+ },
84
+ "description": "Run a Habitat local-hook entrypoint",
85
+ "flags": {},
86
+ "hasDynamicHelp": false,
87
+ "hiddenAliases": [],
88
+ "id": "hook",
89
+ "pluginAlias": "@habitat-ai/cli",
90
+ "pluginName": "@habitat-ai/cli",
91
+ "pluginType": "core",
92
+ "strict": true,
93
+ "enableJsonFlag": false,
94
+ "isESM": true,
95
+ "relativePath": [
96
+ "dist",
97
+ "commands",
98
+ "hook.js"
99
+ ]
100
100
  }
101
101
  },
102
- "version": "0.4.0"
102
+ "version": "0.4.2"
103
103
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@habitat-ai/cli",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -45,7 +45,7 @@
45
45
  "test": "vitest run --project habitat-cli"
46
46
  },
47
47
  "dependencies": {
48
- "@habitat-ai/sdk": "0.4.0",
48
+ "@habitat-ai/sdk": "0.4.2",
49
49
  "@nx/devkit": "23.1.0",
50
50
  "@oclif/core": "^4.13.2",
51
51
  "@oclif/plugin-help": "^6.2.27",