@haus-tech/haus-workflow 0.19.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.21.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.20.0...v0.21.0) (2026-06-11)
4
+
5
+ ### Features
6
+
7
+ - **clone:** add project:cloneandsetup command for cloning and setting up repos ([#94](https://github.com/WeAreHausTech/haus-workflow/issues/94)) ([3e7d279](https://github.com/WeAreHausTech/haus-workflow/commit/3e7d2799433106615a1bf25e94be3402f4eb69d2))
8
+
9
+ ### Bug Fixes
10
+
11
+ - **audit:** correctness hardening ([#90](https://github.com/WeAreHausTech/haus-workflow/issues/90)) ([823b98d](https://github.com/WeAreHausTech/haus-workflow/commit/823b98dc9e4deb2d28ecc5b57b721e80af555a05))
12
+
13
+ ## [0.20.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.19.0...v0.20.0) (2026-06-11)
14
+
15
+ ### Features
16
+
17
+ - **clone:** enhance user confirmation prompts before cloning repositories ([#93](https://github.com/WeAreHausTech/haus-workflow/issues/93)) ([1f3e6b4](https://github.com/WeAreHausTech/haus-workflow/commit/1f3e6b4c4e97b2ab7b979ab4fbcab8ce6cda3f32))
18
+
3
19
  ## [0.19.0](https://github.com/WeAreHausTech/haus-workflow/compare/v0.18.2...v0.19.0) (2026-06-11)
4
20
 
5
21
  ### Features
package/dist/cli.js CHANGED
@@ -332,26 +332,39 @@ async function readSettings() {
332
332
  async function writeSettings(settings) {
333
333
  await writeJson(settingsJsonPath(), settings);
334
334
  }
335
+ function collectEventHookCommands(entries) {
336
+ const cmds = /* @__PURE__ */ new Set();
337
+ for (const entry of entries) {
338
+ for (const h of entry.hooks ?? []) {
339
+ if (h.command) cmds.add(h.command);
340
+ }
341
+ }
342
+ return cmds;
343
+ }
335
344
  function mergeHooks(settings, fragments) {
336
345
  const existing = settings._haus?.hooks ?? [];
337
346
  const existingCommands = settings._haus?.hookCommands ?? [];
338
- const existingSet = new Set(existing);
339
347
  const updated = { ...settings };
340
348
  updated.hooks = { ...settings.hooks ?? {} };
341
349
  const addedIds = [];
342
350
  const addedCommands = [];
343
351
  for (const fragment of fragments) {
344
352
  if (fragment.gate !== "keep") continue;
345
- if (existingSet.has(fragment.id)) continue;
346
353
  const event = fragment.event;
354
+ const eventEntries = updated.hooks[event] ?? [];
355
+ const presentCommands = collectEventHookCommands(eventEntries);
356
+ if (presentCommands.has(fragment.command)) {
357
+ if (!existingCommands.includes(fragment.command)) addedCommands.push(fragment.command);
358
+ continue;
359
+ }
347
360
  if (!updated.hooks[event]) updated.hooks[event] = [];
348
361
  const entry = {
349
362
  hooks: [{ type: "command", command: fragment.command }]
350
363
  };
351
364
  if (fragment.matcher) entry.matcher = fragment.matcher;
352
365
  updated.hooks[event] = [...updated.hooks[event] ?? [], entry];
353
- addedIds.push(fragment.id);
354
- addedCommands.push(fragment.command);
366
+ if (!existing.includes(fragment.id)) addedIds.push(fragment.id);
367
+ if (!existingCommands.includes(fragment.command)) addedCommands.push(fragment.command);
355
368
  }
356
369
  updated._haus = {
357
370
  hooks: [...existing, ...addedIds],
@@ -703,6 +716,27 @@ function catalogItemContentPath(contentRoot, item) {
703
716
  import path7 from "path";
704
717
  import fg2 from "fast-glob";
705
718
  import fs4 from "fs-extra";
719
+
720
+ // src/claude/managed-template.ts
721
+ function normaliseLF(content2) {
722
+ return content2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
723
+ }
724
+ var SCHEMA_VERSION = 1;
725
+ function parseHausManagedHeader(line2) {
726
+ const match = line2.match(/<!-- HAUS-MANAGED id=([\w.:-]+)/);
727
+ if (!match) return null;
728
+ const vMatch = line2.match(/\bv=(\d+)/);
729
+ const sourceMatch = line2.match(/\bsource=([^\s]+)/);
730
+ const hashMatch = line2.match(/hash=(sha256-[a-f0-9]+)/);
731
+ return {
732
+ id: match[1],
733
+ v: vMatch ? Number(vMatch[1]) : void 0,
734
+ source: sourceMatch?.[1],
735
+ hash: hashMatch?.[1]
736
+ };
737
+ }
738
+
739
+ // src/update/hash-installed.ts
706
740
  var EMPTY_LOCK_PATHS_TOKEN = "haus-lock:empty-paths";
707
741
  async function hashInstalledPaths(root, relPaths) {
708
742
  if (relPaths.length === 0) {
@@ -716,7 +750,7 @@ async function hashInstalledPaths(root, relPaths) {
716
750
  const stat = await fs4.stat(abs);
717
751
  if (stat.isFile()) {
718
752
  const body = await fs4.readFile(abs, "utf8");
719
- fileDigests.push({ rel, digest: hashText(body) });
753
+ fileDigests.push({ rel, digest: hashText(normaliseLF(body)) });
720
754
  continue;
721
755
  }
722
756
  if (!stat.isDirectory()) continue;
@@ -725,7 +759,7 @@ async function hashInstalledPaths(root, relPaths) {
725
759
  const relFile = path7.join(rel, sub).replace(/\\/g, "/");
726
760
  const absFile = path7.join(abs, sub);
727
761
  const body = await fs4.readFile(absFile, "utf8");
728
- fileDigests.push({ rel: relFile, digest: hashText(body) });
762
+ fileDigests.push({ rel: relFile, digest: hashText(normaliseLF(body)) });
729
763
  }
730
764
  }
731
765
  if (fileDigests.length === 0) {
@@ -1100,25 +1134,11 @@ async function writeWorkflowConfig(root, dryRun, opts = {}) {
1100
1134
 
1101
1135
  // src/claude/write-workflow.ts
1102
1136
  import fs10 from "fs-extra";
1103
-
1104
- // src/claude/managed-template.ts
1105
- function normaliseLF(content2) {
1106
- return content2.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1107
- }
1108
- function parseHausManagedHeader(line2) {
1109
- const match = line2.match(/<!-- HAUS-MANAGED id=([\w.:-]+)/);
1110
- if (!match) return null;
1111
- const hashMatch = line2.match(/hash=(sha256-[a-f0-9]+)/);
1112
- return { id: match[1], hash: hashMatch?.[1] };
1113
- }
1114
-
1115
- // src/claude/write-workflow.ts
1116
1137
  var STABLE_ID = "template.workflow";
1117
- var SCHEMA_VERSION = "1";
1118
1138
  function makeWorkflowHeader(pkgVersion, contentHash) {
1119
1139
  return `<!-- HAUS-MANAGED id=${STABLE_ID} v=${SCHEMA_VERSION} source=@haus-tech/haus-workflow@${pkgVersion} hash=${contentHash} -->`;
1120
1140
  }
1121
- async function writeWorkflow(root, pkgVersion, dryRun) {
1141
+ async function writeWorkflow(root, pkgVersion, dryRun, force = false) {
1122
1142
  const templateContent = await readWorkflowTemplate({ dryRun });
1123
1143
  if (templateContent === null) {
1124
1144
  warn(
@@ -1144,8 +1164,14 @@ ${templateContent}`;
1144
1164
  warn(`${printable}: HAUS-MANAGED id mismatch (expected ${STABLE_ID}) \u2014 skipping`);
1145
1165
  return null;
1146
1166
  }
1167
+ if (parsed.v !== void 0 && parsed.v > SCHEMA_VERSION) {
1168
+ warn(
1169
+ `${printable}: written by a newer haus (template v${parsed.v}) \u2014 upgrade the CLI to manage it`
1170
+ );
1171
+ return null;
1172
+ }
1147
1173
  const existingContent = existing.slice(firstLine.length + 1);
1148
- if (parsed.hash && hashText(normaliseLF(existingContent)) !== parsed.hash) {
1174
+ if (parsed.hash && hashText(normaliseLF(existingContent)) !== parsed.hash && !force) {
1149
1175
  warn(`${printable}: content modified by user \u2014 skipping. Use --force to overwrite.`);
1150
1176
  return null;
1151
1177
  }
@@ -1191,7 +1217,7 @@ async function writeClaudeFiles(root, dryRun, selectedIds, opts = {}) {
1191
1217
  claudePath(root, "commands", "haus-review.md")
1192
1218
  ];
1193
1219
  const rootClaudeMdPath = await writeRootClaudeMd(root, dryRun);
1194
- const workflowPath = await writeWorkflow(root, hausVersion2, dryRun);
1220
+ const workflowPath = await writeWorkflow(root, hausVersion2, dryRun, opts.force);
1195
1221
  const workflowConfigPath = await writeWorkflowConfig(root, dryRun, {
1196
1222
  refill: opts.refillConfig
1197
1223
  });
@@ -1417,7 +1443,8 @@ async function runApply(options) {
1417
1443
  }
1418
1444
  }
1419
1445
  const files = await writeClaudeFiles(root, isDryRun, selectedIds, {
1420
- refillConfig: options.refillConfig
1446
+ refillConfig: options.refillConfig,
1447
+ force: options.force
1421
1448
  });
1422
1449
  if (isDryRun) {
1423
1450
  log(`Dry-run complete \u2014 ${files.length} file(s) planned, none written. Run --write to apply.`);
@@ -2796,35 +2823,45 @@ async function runDoctor(options) {
2796
2823
  "haus apply --write"
2797
2824
  );
2798
2825
  } else {
2799
- const workflowContent = await readText(workflowPath);
2800
- const firstLine = workflowContent?.split("\n")[0] ?? "";
2826
+ const workflowContent = await readText(workflowPath) ?? "";
2827
+ const firstLine = workflowContent.split("\n")[0] ?? "";
2801
2828
  if (!firstLine.includes("HAUS-MANAGED")) {
2802
2829
  ok("- .haus-workflow/WORKFLOW.md: OK (user-owned)");
2803
2830
  } else {
2804
2831
  const storedHashMatch = firstLine.match(/hash=(sha256-[a-f0-9]+)/);
2805
- const cachePath = path20.join(getCacheDir(), "templates/agentic-workflow-standard.md");
2806
- const bundledPath = path20.join(
2807
- packageRoot(),
2808
- "library",
2809
- "global",
2810
- "templates",
2811
- "agentic-workflow-standard.md"
2812
- );
2813
- const templatePath = await fs14.pathExists(cachePath) ? cachePath : bundledPath;
2814
- const templateContent = await readText(templatePath);
2815
- if (storedHashMatch && templateContent) {
2816
- const currentHash = hashText(normaliseLF(templateContent));
2817
- if (storedHashMatch[1] !== currentHash) {
2818
- flag(
2819
- "- .haus-workflow/WORKFLOW.md: stale (template updated \u2014 run `haus apply --write`)",
2820
- "The workflow standard is out of date",
2821
- "haus apply --write"
2822
- );
2832
+ const bodyContent = workflowContent.slice(firstLine.length + 1);
2833
+ const onDiskBodyHash = hashText(normaliseLF(bodyContent));
2834
+ if (storedHashMatch && onDiskBodyHash !== storedHashMatch[1]) {
2835
+ flag(
2836
+ "- .haus-workflow/WORKFLOW.md: modified locally (run `haus apply --write --force` to restore)",
2837
+ "The workflow standard file was edited after haus wrote it",
2838
+ "haus apply --write --force"
2839
+ );
2840
+ } else {
2841
+ const cachePath = path20.join(getCacheDir(), "templates/agentic-workflow-standard.md");
2842
+ const bundledPath = path20.join(
2843
+ packageRoot(),
2844
+ "library",
2845
+ "global",
2846
+ "templates",
2847
+ "agentic-workflow-standard.md"
2848
+ );
2849
+ const templatePath = await fs14.pathExists(cachePath) ? cachePath : bundledPath;
2850
+ const templateContent = await readText(templatePath);
2851
+ if (storedHashMatch && templateContent) {
2852
+ const currentHash = hashText(normaliseLF(templateContent));
2853
+ if (storedHashMatch[1] !== currentHash) {
2854
+ flag(
2855
+ "- .haus-workflow/WORKFLOW.md: stale (template updated \u2014 run `haus apply --write`)",
2856
+ "The workflow standard is out of date",
2857
+ "haus apply --write"
2858
+ );
2859
+ } else {
2860
+ ok("- .haus-workflow/WORKFLOW.md: OK");
2861
+ }
2823
2862
  } else {
2824
2863
  ok("- .haus-workflow/WORKFLOW.md: OK");
2825
2864
  }
2826
- } else {
2827
- ok("- .haus-workflow/WORKFLOW.md: OK");
2828
2865
  }
2829
2866
  }
2830
2867
  }
@@ -3036,7 +3073,7 @@ async function confirm(question) {
3036
3073
  async function readChangedFiles(root) {
3037
3074
  if (process.env.HAUS_DISABLE_GIT_SIGNALS === "1") return [];
3038
3075
  try {
3039
- const result = await runGit(["diff", "--name-only"], { cwd: root });
3076
+ const result = await runGit(["diff", "--name-only"], { cwd: root, timeout: 3e3 });
3040
3077
  if (result.exitCode !== 0) {
3041
3078
  return [];
3042
3079
  }
@@ -5090,7 +5127,7 @@ program.command("apply").option("--dry-run").option("--write").option("--select"
5090
5127
  ).option(
5091
5128
  "--refill-config",
5092
5129
  "Fill still-blank fields in an existing workflow-config.md without touching edited ones"
5093
- ).action(runApply);
5130
+ ).option("--force", "Overwrite user-modified managed workflow files").action(runApply);
5094
5131
  program.command("undo").option("-y, --yes", "Skip confirmation").action(runUndo);
5095
5132
  program.command("explain-recommendation").option("--json").action(runExplainRecommendation);
5096
5133
  program.command("context").option("--task <task>").option("--from-hook").option("--json").option("--verbose").action(runContext);
@@ -2,6 +2,8 @@ Clone repositories for this project. Per-repo setup (install, Docker, `.env`) is
2
2
 
3
3
  Cloning a single repo is always `haus clone <url> [dir]`. This command picks _which_ repos to clone and runs that primitive for each. There are two modes, chosen by whether a name was given.
4
4
 
5
+ **Always ask before cloning — never assume.** The user may already have the repos on disk. Do not start cloning until they have confirmed. A missing `repos.local.json` does **not** mean they want a fresh clone; it just means nothing is recorded yet — you must still ask.
6
+
5
7
  ## Mode A — a project name was given (`project:clone <name>`)
6
8
 
7
9
  Find one repo by name on GitHub and clone it. Does **not** require a workspace or `repos.manifest.json`.
@@ -13,9 +15,9 @@ Find one repo by name on GitHub and clone it. Does **not** require a workspace o
13
15
  If that returns nothing, retry **without** `--owner` (a broader, all-of-GitHub search) and tell the user you widened it.
14
16
  4. Decide from the results:
15
17
  - **0 matches** — tell the user nothing matched `<name>`; offer to try a different name or broaden. Stop.
16
- - **1 match** — show `fullName` + description and confirm "Clone this one?" before proceeding.
18
+ - **1 match** — show `fullName` + description and ask the user to confirm before cloning.
17
19
  - **2+ matches** — use `AskUserQuestion` to let the user pick which repo (list each `fullName` with its description; private repos noted). Include a final option like "None of these — search again / broaden" so they can refine.
18
- 5. Clone the chosen repo with `haus clone <url> [dir]`, using the `url` from the search result. Default target is a folder named after the repo under the current directory; confirm where it will land before running. Quote the exact command first.
20
+ 5. Once the user has confirmed both the repo and where it should land, clone it with `haus clone <url> [dir]` using the `url` from the search result (default target is a folder named after the repo under the current directory). Quote the exact command first.
19
21
  6. Report the result (cloned / skipped if already present / failed). Remind the user that installing dependencies and configuring the repo is still a manual step for now.
20
22
 
21
23
  ## Mode B — no name was given (`project:clone`)
@@ -24,9 +26,11 @@ Clone a whole **workspace** from its manifest. Workspace-only (a `repos.manifest
24
26
 
25
27
  1. Confirm `repos.manifest.json` exists at the workspace root. If not, tell the user this mode is for multi-repo workspaces (or they can pass a `<name>` to clone a single repo) and stop.
26
28
  2. Read `repos.manifest.json`. Each entry has an `id`, a `folder`, and a git URL (`repo`). If entries have no `repo` URL, ask the user to add them (or supply the URLs) — `haus clone` needs a URL per repo.
27
- 3. Read `repos.local.json` if present — its `pathOverrides` map (`folder` → absolute path) marks repos the user already has locally.
28
- 4. Ask the user, via `AskUserQuestion`, how to obtain the repos:
29
+ 3. Read `repos.local.json` if present — its `pathOverrides` map (`folder` → absolute path) marks repos the user already has locally and does not want re-cloned.
30
+ 4. **Always ask first**, via `AskUserQuestion` never skip this, even when `repos.local.json` is absent or every repo is missing locally:
29
31
  - **Clean clone** — clone every manifest repo fresh into its `folder` under the workspace.
30
- - **Reuse local** — skip any repo already in `repos.local.json` `pathOverrides`; clone only the rest.
31
- 5. For each repo to clone, run (quoting it first): `haus clone <repo-url> <folder>` from the workspace root. Offer `--dry-run` first if the user wants a preview. If one repo fails, report it and continue to the next.
32
- 6. After the loop, report which repos were cloned, skipped (already present or reused local), and failed. Remind the user that installing dependencies and configuring each repo (`.env`, services) is still a manual step for now.
32
+ - **I already have some or all of them** — the user has clones elsewhere on disk. Ask where they live, then for each repo found there, record it in `repos.local.json` `pathOverrides` (`folder` → absolute path) so it's reused instead of cloned; clone only the repos that aren't found. (You can match by folder name under the directory they give, confirming each.)
33
+ - **Cancel** do nothing.
34
+ 5. Show the concrete plan before touching anything: list which repos will be cloned (and into which `folder`) and which will be reused/skipped. Get a final go-ahead.
35
+ 6. For each repo to clone, run (quoting it first): `haus clone <repo-url> <folder>` from the workspace root. Offer `--dry-run` first if the user wants a preview. If one repo fails, report it and continue to the next.
36
+ 7. After the loop, report which repos were cloned, reused (local), skipped (already present), and failed. Remind the user that installing dependencies and configuring each repo (`.env`, services) is still a manual step for now.
@@ -0,0 +1,49 @@
1
+ Clone a project's repos **and** set each one up locally — node version, dependencies, env scaffold. This is `project:clone` followed by a per-repo setup pass.
2
+
3
+ **Always ask before doing work — never assume.** Cloning and setup both run things that take time, hit the network, and need auth; confirm with the user before each phase, and respect repos they already have.
4
+
5
+ ## Step 1 — Clone
6
+
7
+ Run the full `project:clone` flow first by following `~/.claude/commands/haus-clone.md` end to end — whichever mode applies:
8
+
9
+ - A **name** was given → it finds and clones that one repo from GitHub.
10
+ - **No name** → it clones the workspace's repos from `repos.manifest.json` (asking clean-clone vs reuse-local first).
11
+
12
+ When that finishes you have a set of repos on disk (freshly cloned and/or reused-local). Carry that list into Step 2.
13
+
14
+ ## Step 2 — Confirm the setup pass
15
+
16
+ Before running any setup:
17
+
18
+ 1. List the repos you're about to set up and what each will run (node version select, dependency install, etc.). Get a go-ahead. For repos that were **reused** from an existing local clone, ask whether to (re)run setup there too — they may already be set up.
19
+ 2. Check `NODE_AUTH_TOKEN` is exported if any repo depends on private `@`-scoped packages (e.g. `@haus-storefront-*`, `@haus-tech/*`). If it's missing, tell the user to set it first — those installs will fail without it. Let them decide whether to continue or stop.
20
+
21
+ ## Step 3 — Set up each repo
22
+
23
+ For each repo directory, run its setup **in that directory**, detecting what's needed from the repo's own files — don't assume a stack. Run a repo's steps in a single login shell so the selected node version stays active for the install. A robust pattern:
24
+
25
+ ```
26
+ bash -lc 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; cd "<repo>" && nvm install && corepack enable && yarn install'
27
+ ```
28
+
29
+ Adjust per repo:
30
+
31
+ 1. **Node version.** If `.nvmrc` (or `engines.node` in `package.json`) is present, select it with `nvm install` (reads `.nvmrc`, installs the version if missing, then switches to it). If the user uses `fnm` instead, `fnm use --install-if-missing`. If neither is available, tell the user the required version and continue on the current node.
32
+ 2. **JS dependencies.** Enable the pinned package manager with `corepack enable`, then install based on what's present:
33
+ - `yarn.lock` or `packageManager: "yarn@…"` → `yarn install`
34
+ - `pnpm-lock.yaml` → `pnpm install`
35
+ - `package-lock.json` → `npm install`
36
+ - no JS manifest → skip
37
+ 3. **PHP dependencies.** If `composer.json` is present and `composer` is installed → `composer install`. If composer is missing, note it and skip.
38
+ 4. **Env scaffold.** If `.env.example` exists and `.env` does not → copy `.env.example` to `.env` (never overwrite an existing `.env`). Tell the user the real values still need filling.
39
+
40
+ If a repo's setup fails, report the error and **continue to the next repo** — don't abort the whole run.
41
+
42
+ ## Step 4 — Report
43
+
44
+ Summarise per repo: node version used, dependency install result, composer (if any), env seeded. Then list what's still manual:
45
+
46
+ - Fill in each `.env` with real values (cross-repo values must match — see the workspace's environment docs).
47
+ - Start Docker services and dev servers in dependency order — see the workspace's local-development docs.
48
+
49
+ **Do not** start servers or run `docker compose up` / `yarn dev` — this command only prepares the repos.
@@ -21,16 +21,17 @@ The unprefixed verbs (`update`, `catalog`, `install`, `uninstall`) act on **this
21
21
  haus install** (`~/.claude`, npm) — they manage the haus tool itself, like `npm install -g`.
22
22
  The short legacy aliases still work but the names below are canonical.
23
23
 
24
- | Task name (legacy aliases) | Command | Scope | What it does |
25
- | ----------------------------------------------------------------- | ----------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------- |
26
- | `project:init` (`setup`, `init`) | _Setup procedure below_ | project | First-time setup of an **existing** repo: adds AI skills, commands, workflow + project docs |
27
- | `project:clone [name]` (`clone`) | _Clone procedure below_ | project | No name: clone a **workspace**'s repos from `repos.manifest.json`. With a `name`: find & clone one repo by name from GitHub |
28
- | `project:refresh` (`apply`, `refresh`, `claude-md`, `regenerate`) | `haus apply --write` | project | Re-run setup / refresh `.claude/` context + regenerate root `CLAUDE.md` import block |
29
- | `project:doctor` (`doctor`, `check`) | `haus doctor` | project | Check for install drift |
30
- | `update` (`upgrade`) | `haus update` | global | Update npm package + catalog + `~/.claude/` (also refreshes this project) |
31
- | `catalog` | `haus update` | global | Fetch latest catalog (same command as update) |
32
- | `install` (`global`) | `haus install` | global | Seed `~/.claude/` with haus-owned files |
33
- | `uninstall` | `haus uninstall` | global | Remove all haus global files from `~/.claude/` |
24
+ | Task name (legacy aliases) | Command | Scope | What it does |
25
+ | ----------------------------------------------------------------- | ------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------- |
26
+ | `project:init` (`setup`, `init`) | _Setup procedure below_ | project | First-time setup of an **existing** repo: adds AI skills, commands, workflow + project docs |
27
+ | `project:clone [name]` (`clone`) | _Clone procedure below_ | project | No name: clone a **workspace**'s repos from `repos.manifest.json`. With a `name`: find & clone one repo by name from GitHub |
28
+ | `project:cloneandsetup [name]` (`cloneandsetup`) | _Clone & setup procedure below_ | project | Run `project:clone`, then set up each repo locally (node version, deps, `.env`) |
29
+ | `project:refresh` (`apply`, `refresh`, `claude-md`, `regenerate`) | `haus apply --write` | project | Re-run setup / refresh `.claude/` context + regenerate root `CLAUDE.md` import block |
30
+ | `project:doctor` (`doctor`, `check`) | `haus doctor` | project | Check for install drift |
31
+ | `update` (`upgrade`) | `haus update` | global | Update npm package + catalog + `~/.claude/` (also refreshes this project) |
32
+ | `catalog` | `haus update` | global | Fetch latest catalog (same command as update) |
33
+ | `install` (`global`) | `haus install` | global | Seed `~/.claude/` with haus-owned files |
34
+ | `uninstall` | `haus uninstall` | global | Remove all haus global files from `~/.claude/` |
34
35
 
35
36
  ## Step 1 — Determine the task
36
37
 
@@ -51,6 +52,8 @@ Options:
51
52
  (haus update — same command; pulls latest workflow templates and lockfile)
52
53
  5. [project] project:clone [name] — clone repos
53
54
  (no name: clone a workspace from repos.manifest.json; with a name: find & clone one repo by name from GitHub)
55
+ 6. [project] project:cloneandsetup [name] — clone repos, then set them up
56
+ (project:clone, then per-repo node version + dependency install + .env scaffold)
54
57
  ```
55
58
 
56
59
  Map the user's selection to the command from the alias table, then continue to Step 2.
@@ -63,6 +66,8 @@ Run the mapped command via Bash. Quote the exact command you are running before
63
66
 
64
67
  **Exception — `project:clone` (`clone`):** this asks the user a question before running, so it is a short procedure too. Skip to **Clone (`project:clone`)** under Step 3 and follow it.
65
68
 
69
+ **Exception — `project:cloneandsetup` (`cloneandsetup`):** clone followed by a per-repo setup pass, with confirmations. Skip to **Clone & setup (`project:cloneandsetup`)** under Step 3 and follow it.
70
+
66
71
  ## Step 3 — Post-run steps
67
72
 
68
73
  After the command completes, follow the relevant post-run steps below.
@@ -74,7 +79,11 @@ After the command completes, follow the relevant post-run steps below.
74
79
 
75
80
  ### Clone (`project:clone`)
76
81
 
77
- 1. Open and follow `~/.claude/commands/haus-clone.md` — the installed `haus-clone` command. With a `name` argument it finds and clones one matching repo from GitHub; with no argument it clones a workspace's repos from `repos.manifest.json`. Per-repo setup (install, Docker, env) is a separate step that isn't wired yet, so just get the repos in place for now.
82
+ 1. Open and follow `~/.claude/commands/haus-clone.md` — the installed `haus-clone` command. With a `name` argument it finds and clones one matching repo from GitHub; with no argument it clones a workspace's repos from `repos.manifest.json`. This task only clones to also install dependencies, use `project:cloneandsetup`.
83
+
84
+ ### Clone & setup (`project:cloneandsetup`)
85
+
86
+ 1. Open and follow `~/.claude/commands/haus-cloneandsetup.md` — it runs the full `project:clone` flow, then sets up each cloned repo locally: selects the node version (`nvm install` from `.nvmrc`), enables corepack, installs JS/PHP dependencies, and seeds `.env`, confirming before each phase. It does not start servers.
78
87
 
79
88
  ### After `haus apply --write`
80
89
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/haus-workflow",
3
- "version": "0.19.0",
3
+ "version": "0.21.0",
4
4
  "description": "Haus AI workflow CLI for Claude Code.",
5
5
  "type": "module",
6
6
  "bin": {