@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
|
|
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
|
|
2806
|
-
const
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
- **
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
25
|
-
| ----------------------------------------------------------------- |
|
|
26
|
-
| `project:init` (`setup`, `init`) | _Setup procedure below_
|
|
27
|
-
| `project:clone [name]` (`clone`) | _Clone procedure below_
|
|
28
|
-
| `project:
|
|
29
|
-
| `project:
|
|
30
|
-
| `
|
|
31
|
-
| `
|
|
32
|
-
| `
|
|
33
|
-
| `
|
|
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`.
|
|
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
|
|