create-hq 10.7.1 → 10.9.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/commands/invite.md +215 -0
- package/commands/promote.md +161 -0
- package/commands/team-sync.md +431 -0
- package/dist/__tests__/auth.test.d.ts +2 -0
- package/dist/__tests__/auth.test.d.ts.map +1 -0
- package/dist/__tests__/auth.test.js +201 -0
- package/dist/__tests__/auth.test.js.map +1 -0
- package/dist/__tests__/scaffold.test.js +14 -1
- package/dist/__tests__/scaffold.test.js.map +1 -1
- package/dist/admin-onboarding.d.ts.map +1 -1
- package/dist/admin-onboarding.js +11 -0
- package/dist/admin-onboarding.js.map +1 -1
- package/dist/auth.d.ts +58 -13
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +105 -29
- package/dist/auth.js.map +1 -1
- package/dist/fetch-template.d.ts +3 -3
- package/dist/fetch-template.d.ts.map +1 -1
- package/dist/fetch-template.js +93 -71
- package/dist/fetch-template.js.map +1 -1
- package/dist/git.d.ts +12 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +40 -20
- package/dist/git.js.map +1 -1
- package/dist/index.js +0 -0
- package/dist/join-flow.d.ts.map +1 -1
- package/dist/join-flow.js +11 -0
- package/dist/join-flow.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +48 -13
- package/dist/scaffold.js.map +1 -1
- package/dist/team-setup.d.ts +25 -0
- package/dist/team-setup.d.ts.map +1 -1
- package/dist/team-setup.js +113 -0
- package/dist/team-setup.js.map +1 -1
- package/dist/teams-flow.d.ts +7 -2
- package/dist/teams-flow.d.ts.map +1 -1
- package/dist/teams-flow.js +31 -8
- package/dist/teams-flow.js.map +1 -1
- package/dist/ui.d.ts +3 -0
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +79 -3
- package/dist/ui.js.map +1 -1
- package/package.json +3 -2
- package/dist/art.d.ts +0 -17
- package/dist/art.d.ts.map +0 -1
- package/dist/art.js +0 -171
- package/dist/art.js.map +0 -1
- package/dist/cloud.d.ts +0 -26
- package/dist/cloud.d.ts.map +0 -1
- package/dist/cloud.js +0 -126
- package/dist/cloud.js.map +0 -1
- package/dist/tui.d.ts +0 -8
- package/dist/tui.d.ts.map +0 -1
- package/dist/tui.js +0 -86
- package/dist/tui.js.map +0 -1
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
# Sync Team Content
|
|
2
|
+
|
|
3
|
+
Pull latest team content and push local changes for all joined teams. Bidirectional git sync using `gh` CLI for authentication — no manual git operations needed.
|
|
4
|
+
|
|
5
|
+
**Usage:** `/sync` or `/sync --team <slug>` or `/sync --dry-run`
|
|
6
|
+
|
|
7
|
+
**Requires:** `gh` CLI authenticated (`gh auth status`)
|
|
8
|
+
|
|
9
|
+
## Arguments
|
|
10
|
+
|
|
11
|
+
Parse the user's input for:
|
|
12
|
+
- `--team <slug>` — Sync only a specific team by slug (e.g., `--team indigo`)
|
|
13
|
+
- `--dry-run` — Show what would be synced without making changes
|
|
14
|
+
|
|
15
|
+
If no flags, sync all discovered teams.
|
|
16
|
+
|
|
17
|
+
## Process
|
|
18
|
+
|
|
19
|
+
1. Discover teams from `companies/*/team.json`
|
|
20
|
+
2. Verify `gh` CLI is authenticated
|
|
21
|
+
3. For each team: pull remote changes, then push local changes
|
|
22
|
+
4. Sync command symlinks
|
|
23
|
+
5. Report what was pulled and pushed
|
|
24
|
+
|
|
25
|
+
## Steps
|
|
26
|
+
|
|
27
|
+
### 1. Verify gh CLI
|
|
28
|
+
|
|
29
|
+
Check that `gh` is installed and authenticated:
|
|
30
|
+
```bash
|
|
31
|
+
gh auth status 2>&1
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
If `gh` is not found:
|
|
35
|
+
```
|
|
36
|
+
GitHub CLI (gh) is required for team commands.
|
|
37
|
+
Install it: https://cli.github.com
|
|
38
|
+
```
|
|
39
|
+
Stop here.
|
|
40
|
+
|
|
41
|
+
If not authenticated:
|
|
42
|
+
```
|
|
43
|
+
GitHub CLI is not authenticated. Run:
|
|
44
|
+
gh auth login
|
|
45
|
+
Then try /sync again.
|
|
46
|
+
```
|
|
47
|
+
Stop here.
|
|
48
|
+
|
|
49
|
+
Ensure git is configured to use `gh` for HTTPS authentication:
|
|
50
|
+
```bash
|
|
51
|
+
gh auth setup-git 2>&1
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Discover teams
|
|
55
|
+
|
|
56
|
+
Find all team.json files:
|
|
57
|
+
```bash
|
|
58
|
+
find companies/*/team.json -maxdepth 0 2>/dev/null
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
If no files found:
|
|
62
|
+
```
|
|
63
|
+
No teams found. Join a team first:
|
|
64
|
+
npx create-hq
|
|
65
|
+
```
|
|
66
|
+
Stop here.
|
|
67
|
+
|
|
68
|
+
If `--team <slug>` was specified, filter to only `companies/{slug}/team.json`. If that file doesn't exist:
|
|
69
|
+
```
|
|
70
|
+
Team "{slug}" not found. Available teams:
|
|
71
|
+
{list discovered team slugs}
|
|
72
|
+
```
|
|
73
|
+
Stop here.
|
|
74
|
+
|
|
75
|
+
### 3. Sync each team
|
|
76
|
+
|
|
77
|
+
For each team (or the single `--team` target):
|
|
78
|
+
|
|
79
|
+
#### 3a. Read team metadata
|
|
80
|
+
|
|
81
|
+
Read `companies/{slug}/team.json` and extract:
|
|
82
|
+
- `team_name` — human-readable name
|
|
83
|
+
- `team_slug` — directory slug
|
|
84
|
+
|
|
85
|
+
Get the remote URL:
|
|
86
|
+
```bash
|
|
87
|
+
git -C companies/{slug} remote get-url origin
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
If no git remote is configured:
|
|
91
|
+
```
|
|
92
|
+
Team "{slug}" has no git remote configured. Was it set up correctly?
|
|
93
|
+
Try re-joining: npx create-hq
|
|
94
|
+
```
|
|
95
|
+
Skip this team and continue.
|
|
96
|
+
|
|
97
|
+
#### 3b. Check local status
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
git -C companies/{slug} status --short
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Note which files have local modifications (these will be pushed after pulling).
|
|
104
|
+
|
|
105
|
+
#### 3c. Pull remote changes (--dry-run: fetch only)
|
|
106
|
+
|
|
107
|
+
If `--dry-run`:
|
|
108
|
+
```bash
|
|
109
|
+
git -C companies/{slug} fetch origin 2>&1
|
|
110
|
+
git -C companies/{slug} log HEAD..origin/main --oneline 2>/dev/null
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Show what would be pulled:
|
|
114
|
+
```
|
|
115
|
+
[dry-run] Would pull from {team_name}:
|
|
116
|
+
{list of incoming commits}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
If NOT `--dry-run`:
|
|
120
|
+
```bash
|
|
121
|
+
git -C companies/{slug} pull origin main --ff-only 2>&1
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Capture the output. If pull succeeds, parse the output for:
|
|
125
|
+
- "Already up to date." → nothing to report
|
|
126
|
+
- File change summary → report changed files
|
|
127
|
+
|
|
128
|
+
If `--ff-only` fails (diverged history):
|
|
129
|
+
```bash
|
|
130
|
+
git -C companies/{slug} pull origin main --no-rebase 2>&1
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
If the merge pull succeeds (auto-merged), continue to step 3d.
|
|
134
|
+
|
|
135
|
+
If the merge pull fails with conflicts, enter the **conflict resolution flow**:
|
|
136
|
+
|
|
137
|
+
##### Conflict Detection
|
|
138
|
+
|
|
139
|
+
List the conflicting files:
|
|
140
|
+
```bash
|
|
141
|
+
git -C companies/{slug} diff --name-only --diff-filter=U
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
For each conflicting file, show a plain-language summary:
|
|
145
|
+
```
|
|
146
|
+
Sync conflict in {team_name} ({slug}):
|
|
147
|
+
|
|
148
|
+
{N} file(s) have changes on both your machine and the team repo:
|
|
149
|
+
|
|
150
|
+
{filename_1}:
|
|
151
|
+
Your change: {brief description from local side of conflict}
|
|
152
|
+
Team change: {brief description from remote side of conflict}
|
|
153
|
+
|
|
154
|
+
{filename_2}:
|
|
155
|
+
Your change: ...
|
|
156
|
+
Team change: ...
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
To generate descriptions, read each conflicting file and look for `<<<<<<<`, `=======`, `>>>>>>>` markers. Summarize the content between `<<<<<<<` and `=======` as "Your change" and between `=======` and `>>>>>>>` as "Team change". Keep descriptions short and jargon-free.
|
|
160
|
+
|
|
161
|
+
##### Resolution Options
|
|
162
|
+
|
|
163
|
+
Ask the user which resolution strategy to use:
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
How would you like to resolve these conflicts?
|
|
167
|
+
|
|
168
|
+
1. Keep my local version (discard team changes for conflicting files)
|
|
169
|
+
2. Keep team version (discard my local changes for conflicting files)
|
|
170
|
+
3. Let me resolve manually (I'll edit the files, then re-run /sync)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
**Option 1 — Keep local:**
|
|
174
|
+
For each conflicting file:
|
|
175
|
+
```bash
|
|
176
|
+
git -C companies/{slug} checkout --ours -- {filename}
|
|
177
|
+
git -C companies/{slug} add {filename}
|
|
178
|
+
```
|
|
179
|
+
Then complete the merge:
|
|
180
|
+
```bash
|
|
181
|
+
git -C companies/{slug} commit -m "sync: resolved conflicts — kept local versions"
|
|
182
|
+
```
|
|
183
|
+
Report: `Kept your local version for {N} file(s). Merge complete.`
|
|
184
|
+
Continue to step 3d (push).
|
|
185
|
+
|
|
186
|
+
**Option 2 — Keep remote (team):**
|
|
187
|
+
For each conflicting file:
|
|
188
|
+
```bash
|
|
189
|
+
git -C companies/{slug} checkout --theirs -- {filename}
|
|
190
|
+
git -C companies/{slug} add {filename}
|
|
191
|
+
```
|
|
192
|
+
Then complete the merge:
|
|
193
|
+
```bash
|
|
194
|
+
git -C companies/{slug} commit -m "sync: resolved conflicts — kept team versions"
|
|
195
|
+
```
|
|
196
|
+
Report: `Kept team version for {N} file(s). Merge complete.`
|
|
197
|
+
Continue to step 3d (push).
|
|
198
|
+
|
|
199
|
+
**Option 3 — Manual merge:**
|
|
200
|
+
```
|
|
201
|
+
OK — the conflicting files have been left with merge markers.
|
|
202
|
+
Open these files and look for lines like:
|
|
203
|
+
|
|
204
|
+
<<<<<<< HEAD
|
|
205
|
+
(your version)
|
|
206
|
+
=======
|
|
207
|
+
(team version)
|
|
208
|
+
>>>>>>>
|
|
209
|
+
|
|
210
|
+
Edit each file to keep what you want, then delete the marker lines.
|
|
211
|
+
When you're done, run /sync again to complete the merge.
|
|
212
|
+
```
|
|
213
|
+
**Do NOT push for this team.** Skip to the next team. The user will re-run /sync after editing.
|
|
214
|
+
|
|
215
|
+
##### Never Silently Overwrite
|
|
216
|
+
|
|
217
|
+
If at any point the merge would silently overwrite local changes (e.g., a force-pull), **do not proceed**. Always show the user what will change and let them choose. The `--no-rebase` flag ensures git does not rewrite local history.
|
|
218
|
+
|
|
219
|
+
##### Post-Resolution State
|
|
220
|
+
|
|
221
|
+
After resolving (options 1 or 2), verify the working tree is clean:
|
|
222
|
+
```bash
|
|
223
|
+
git -C companies/{slug} status --short
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
If clean: report `Conflicts resolved. Ready to push.` and continue.
|
|
227
|
+
If still dirty: report remaining issues and skip push for this team.
|
|
228
|
+
|
|
229
|
+
#### 3d. Push local changes (--dry-run: show status only)
|
|
230
|
+
|
|
231
|
+
If there are local changes to push (from step 3b):
|
|
232
|
+
|
|
233
|
+
If `--dry-run`:
|
|
234
|
+
```bash
|
|
235
|
+
git -C companies/{slug} diff --stat HEAD 2>/dev/null
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Show what would be pushed:
|
|
239
|
+
```
|
|
240
|
+
[dry-run] Would push from {team_name}:
|
|
241
|
+
{list of local changes}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
If NOT `--dry-run`:
|
|
245
|
+
|
|
246
|
+
First, stage and commit any uncommitted changes:
|
|
247
|
+
```bash
|
|
248
|
+
git -C companies/{slug} add -A
|
|
249
|
+
git -C companies/{slug} diff --cached --quiet || git -C companies/{slug} commit -m "sync: local changes from $(whoami)"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### 3d-pre. Pre-push secrets scan
|
|
253
|
+
|
|
254
|
+
Before pushing, scan the changes for accidental secrets or PII. Compare what will be pushed against the remote:
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
git -C companies/{slug} diff origin/main..HEAD -- . ':!team.json' 2>/dev/null
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Scan the diff output for these patterns:
|
|
261
|
+
|
|
262
|
+
| Pattern | Description |
|
|
263
|
+
|---------|-------------|
|
|
264
|
+
| `(?i)(api[_-]?key\|api[_-]?secret)\s*[:=]\s*\S+` | API keys |
|
|
265
|
+
| `(?i)(password\|passwd\|pwd)\s*[:=]\s*\S+` | Passwords |
|
|
266
|
+
| `(?i)(secret\|token)\s*[:=]\s*['"]?[A-Za-z0-9+/=_-]{20,}` | Tokens/secrets |
|
|
267
|
+
| `-----BEGIN (RSA\|DSA\|EC\|OPENSSH) PRIVATE KEY-----` | Private keys |
|
|
268
|
+
| `(?i)(aws_access_key_id\|aws_secret_access_key)\s*=\s*\S+` | AWS credentials |
|
|
269
|
+
| `ghp_[A-Za-z0-9]{36}\|gho_[A-Za-z0-9]{36}\|ghu_[A-Za-z0-9]{36}` | GitHub tokens |
|
|
270
|
+
| `sk-[A-Za-z0-9]{20,}` | OpenAI/Stripe-style keys |
|
|
271
|
+
| `^\+.*\.env` | .env file additions |
|
|
272
|
+
|
|
273
|
+
Run the scan:
|
|
274
|
+
```bash
|
|
275
|
+
git -C companies/{slug} diff origin/main..HEAD -- . ':!team.json' 2>/dev/null | grep -nE '(api[_-]?key|api[_-]?secret|password|passwd|pwd|secret|token)\s*[:=]|-----BEGIN .* PRIVATE KEY-----|aws_(access_key_id|secret_access_key)\s*=|ghp_[A-Za-z0-9]{36}|gho_[A-Za-z0-9]{36}|ghu_[A-Za-z0-9]{36}|sk-[A-Za-z0-9]{20,}' || true
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**If matches found:**
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
Pre-push security scan found potential secrets:
|
|
282
|
+
|
|
283
|
+
{filename}:{line}: {matched pattern preview}
|
|
284
|
+
{filename}:{line}: {matched pattern preview}
|
|
285
|
+
|
|
286
|
+
These look like they might contain sensitive data (API keys, tokens, passwords, or private keys).
|
|
287
|
+
Pushing secrets to a shared repo is hard to undo — they persist in git history.
|
|
288
|
+
|
|
289
|
+
Options:
|
|
290
|
+
1. Remove the sensitive data and re-run /sync
|
|
291
|
+
2. Push anyway (I've verified these are safe to share)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
If the user chooses option 1: skip the push for this team. The user will edit files and re-run /sync.
|
|
295
|
+
If the user chooses option 2: continue to push.
|
|
296
|
+
|
|
297
|
+
**If no matches found:** Continue to push silently (no output needed).
|
|
298
|
+
|
|
299
|
+
#### 3d-push. Push to remote
|
|
300
|
+
|
|
301
|
+
Then push:
|
|
302
|
+
```bash
|
|
303
|
+
git -C companies/{slug} push origin main 2>&1
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
If push fails because remote has new changes (non-fast-forward):
|
|
307
|
+
```
|
|
308
|
+
Remote has new changes. Pulling first, then retrying push...
|
|
309
|
+
```
|
|
310
|
+
Pull again (step 3c flow). If pull triggers conflicts, enter the conflict resolution flow above. After a clean pull, retry the push once:
|
|
311
|
+
```bash
|
|
312
|
+
git -C companies/{slug} push origin main 2>&1
|
|
313
|
+
```
|
|
314
|
+
If the retry also fails, report the error and skip this team:
|
|
315
|
+
```
|
|
316
|
+
Push failed for {team_name} after retry. Error: {error message}
|
|
317
|
+
You can try again later with /sync --team {slug}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### 4. Sync command symlinks
|
|
321
|
+
|
|
322
|
+
After all teams have been synced, manage command symlinks so team-distributed commands are available as slash commands.
|
|
323
|
+
|
|
324
|
+
#### 4a. Scan for team commands
|
|
325
|
+
|
|
326
|
+
For each synced team, check if the team directory contains distributed commands:
|
|
327
|
+
```bash
|
|
328
|
+
ls companies/{slug}/.claude/commands/*.md 2>/dev/null
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
If the directory doesn't exist or has no `.md` files, skip this team for symlink management.
|
|
332
|
+
|
|
333
|
+
#### 4b. Create symlinks for new commands
|
|
334
|
+
|
|
335
|
+
For each `.md` file found in `companies/{slug}/.claude/commands/`:
|
|
336
|
+
|
|
337
|
+
1. Determine the symlink name using the pattern `{slug}--{command}.md` (double-dash separates team slug from command name). For example: `companies/acme/.claude/commands/deploy.md` → `.claude/commands/acme--deploy.md`
|
|
338
|
+
|
|
339
|
+
2. Check if the symlink target already exists at `.claude/commands/{slug}--{command}.md`:
|
|
340
|
+
- If it's already a symlink pointing to the correct source → skip (already linked)
|
|
341
|
+
- If it exists but is NOT a symlink (a real file or symlink to wrong target) → warn and skip:
|
|
342
|
+
```
|
|
343
|
+
Skipping {slug}--{command}.md — file already exists (not a team symlink)
|
|
344
|
+
```
|
|
345
|
+
- If it doesn't exist → create the symlink:
|
|
346
|
+
```bash
|
|
347
|
+
ln -s "../../companies/{slug}/.claude/commands/{command}.md" ".claude/commands/{slug}--{command}.md"
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
3. Track linked commands for the report.
|
|
351
|
+
|
|
352
|
+
**Note on relative paths:** Symlinks use relative paths (`../../companies/...`) so they work regardless of HQ's absolute location. The path is relative from `.claude/commands/` to `companies/{slug}/.claude/commands/`.
|
|
353
|
+
|
|
354
|
+
If `--dry-run`:
|
|
355
|
+
```
|
|
356
|
+
[dry-run] Would link commands for {team_name}:
|
|
357
|
+
{slug}--{command}.md → companies/{slug}/.claude/commands/{command}.md
|
|
358
|
+
```
|
|
359
|
+
Do not create actual symlinks.
|
|
360
|
+
|
|
361
|
+
#### 4c. Remove stale symlinks
|
|
362
|
+
|
|
363
|
+
Scan `.claude/commands/` for symlinks that match the team pattern (`{slug}--*.md`) but whose targets no longer exist (the source command was removed from the team repo):
|
|
364
|
+
|
|
365
|
+
```bash
|
|
366
|
+
for link in .claude/commands/{slug}--*.md; do
|
|
367
|
+
if [ -L "$link" ] && [ ! -e "$link" ]; then
|
|
368
|
+
rm "$link"
|
|
369
|
+
# Track as unlinked for report
|
|
370
|
+
fi
|
|
371
|
+
done
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Also remove symlinks for commands that were removed from the team's `.claude/commands/` directory — compare the set of existing symlinks against the set of current source files:
|
|
375
|
+
|
|
376
|
+
```bash
|
|
377
|
+
# Get current team commands
|
|
378
|
+
CURRENT=$(ls companies/{slug}/.claude/commands/*.md 2>/dev/null | xargs -I{} basename {})
|
|
379
|
+
# Get current symlinks for this team
|
|
380
|
+
LINKED=$(ls -la .claude/commands/{slug}--*.md 2>/dev/null | grep "^l" | awk '{print $NF}' | xargs -I{} basename {})
|
|
381
|
+
# Any symlink not matching a current command → remove
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
If `--dry-run`, show what would be removed without removing.
|
|
385
|
+
|
|
386
|
+
#### 4d. Symlink summary (per team)
|
|
387
|
+
|
|
388
|
+
Collect results for the final report:
|
|
389
|
+
- Commands linked (new symlinks created)
|
|
390
|
+
- Commands already linked (unchanged)
|
|
391
|
+
- Commands unlinked (stale symlinks removed)
|
|
392
|
+
- Commands skipped (name collision)
|
|
393
|
+
|
|
394
|
+
### 5. Report results
|
|
395
|
+
|
|
396
|
+
After syncing all teams, display a summary:
|
|
397
|
+
|
|
398
|
+
```
|
|
399
|
+
Sync complete:
|
|
400
|
+
|
|
401
|
+
{team_name} ({slug}):
|
|
402
|
+
Pulled: {N} files changed ({list or "up to date"})
|
|
403
|
+
Pushed: {N} files changed ({list or "nothing to push"})
|
|
404
|
+
|
|
405
|
+
{team_name_2} ({slug_2}):
|
|
406
|
+
Pulled: ...
|
|
407
|
+
Pushed: ...
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
If `--dry-run`:
|
|
411
|
+
```
|
|
412
|
+
Dry run complete — no changes were made.
|
|
413
|
+
|
|
414
|
+
{team_name} ({slug}):
|
|
415
|
+
Would pull: {N} incoming commits
|
|
416
|
+
Would push: {N} local changes
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Security Notes
|
|
420
|
+
|
|
421
|
+
- Git authentication is handled by `gh auth setup-git`, which configures a credential helper that delegates to `gh`. No tokens are stored on disk or passed via environment variables.
|
|
422
|
+
- No askpass scripts, no credential.helper overrides, no GIT_TOKEN env vars.
|
|
423
|
+
- `gh` stores credentials in the OS keychain (macOS Keychain, Windows Credential Manager) and handles token refresh transparently.
|
|
424
|
+
|
|
425
|
+
## Troubleshooting
|
|
426
|
+
|
|
427
|
+
- **"gh: command not found"** — Install GitHub CLI: https://cli.github.com
|
|
428
|
+
- **"not logged into any GitHub hosts"** — Run `gh auth login`
|
|
429
|
+
- **"No git remote"** — Team directory wasn't set up correctly; re-join the team
|
|
430
|
+
- **"Permission denied" on push** — Your GitHub account may not have write access to this repo
|
|
431
|
+
- **Merge conflicts** — See conflict messages; resolve manually or wait for `/sync` conflict resolution
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/auth.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach, afterAll } from "vitest";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import * as os from "os";
|
|
4
|
+
import * as fs from "fs";
|
|
5
|
+
/**
|
|
6
|
+
* Unit tests for auth.ts:
|
|
7
|
+
* - githubApi error handling (403 on /user/installations)
|
|
8
|
+
* - ~/.hq/app-token.json persistence (load / save / clear)
|
|
9
|
+
* - Token validation via /user/installations probe
|
|
10
|
+
*/
|
|
11
|
+
// ─── Mocks ─────────────────────────────────────────────────────────────────
|
|
12
|
+
const mockFetch = vi.fn();
|
|
13
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
14
|
+
// Mock child_process so module-level execSync calls in auth.ts don't run
|
|
15
|
+
vi.mock("child_process", () => ({
|
|
16
|
+
exec: vi.fn(),
|
|
17
|
+
execSync: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
// Import after mocks are in place
|
|
20
|
+
const { githubApi, loadGitHubAuth, saveGitHubAuth, clearGitHubAuth, isGitHubAuthValid, isAppScopedToken, HQ_APP_TOKEN_PATH, } = await import("../auth.js");
|
|
21
|
+
// ─── Fixtures ──────────────────────────────────────────────────────────────
|
|
22
|
+
const fakeAuth = {
|
|
23
|
+
access_token: "ghu_fake_app_token",
|
|
24
|
+
login: "testuser",
|
|
25
|
+
id: 12345,
|
|
26
|
+
name: "Test User",
|
|
27
|
+
email: "test@example.com",
|
|
28
|
+
issued_at: new Date().toISOString(),
|
|
29
|
+
};
|
|
30
|
+
// Use a temp directory so tests don't touch the real ~/.hq/
|
|
31
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-hq-auth-test-"));
|
|
32
|
+
const tmpTokenPath = path.join(tmpDir, "app-token.json");
|
|
33
|
+
// ─── githubApi ─────────────────────────────────────────────────────────────
|
|
34
|
+
describe("githubApi", () => {
|
|
35
|
+
beforeEach(() => mockFetch.mockReset());
|
|
36
|
+
it("throws a user-friendly message on 403 for /user/installations", async () => {
|
|
37
|
+
mockFetch.mockResolvedValueOnce({
|
|
38
|
+
ok: false,
|
|
39
|
+
status: 403,
|
|
40
|
+
text: async () => JSON.stringify({
|
|
41
|
+
message: "You must authenticate with an access token authorized to a GitHub App in order to list installations",
|
|
42
|
+
documentation_url: "https://docs.github.com/rest/apps/installations#list-app-installations-accessible-to-the-user-access-token",
|
|
43
|
+
status: "403",
|
|
44
|
+
}),
|
|
45
|
+
});
|
|
46
|
+
await expect(githubApi("/user/installations?per_page=100", fakeAuth)).rejects.toThrow(/signed in with a regular GitHub token/i);
|
|
47
|
+
});
|
|
48
|
+
it("includes the re-run hint in the 403 installations error", async () => {
|
|
49
|
+
mockFetch.mockResolvedValueOnce({
|
|
50
|
+
ok: false,
|
|
51
|
+
status: 403,
|
|
52
|
+
text: async () => JSON.stringify({
|
|
53
|
+
message: "You must authenticate with an access token authorized to a GitHub App",
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
await expect(githubApi("/user/installations?per_page=100", fakeAuth)).rejects.toThrow(/npx create-hq/);
|
|
57
|
+
});
|
|
58
|
+
it("preserves the raw error for non-installation 403s", async () => {
|
|
59
|
+
mockFetch.mockResolvedValueOnce({
|
|
60
|
+
ok: false,
|
|
61
|
+
status: 403,
|
|
62
|
+
text: async () => JSON.stringify({ message: "Resource not accessible by integration" }),
|
|
63
|
+
});
|
|
64
|
+
await expect(githubApi("/orgs/acme/repos", fakeAuth)).rejects.toThrow(/GitHub API 403 \/orgs\/acme\/repos/);
|
|
65
|
+
});
|
|
66
|
+
it("throws the raw error for non-403 failures", async () => {
|
|
67
|
+
mockFetch.mockResolvedValueOnce({
|
|
68
|
+
ok: false,
|
|
69
|
+
status: 404,
|
|
70
|
+
text: async () => JSON.stringify({ message: "Not Found" }),
|
|
71
|
+
});
|
|
72
|
+
await expect(githubApi("/user/installations?per_page=100", fakeAuth)).rejects.toThrow(/GitHub API 404/);
|
|
73
|
+
});
|
|
74
|
+
it("returns parsed JSON on success", async () => {
|
|
75
|
+
mockFetch.mockResolvedValueOnce({
|
|
76
|
+
ok: true,
|
|
77
|
+
status: 200,
|
|
78
|
+
json: async () => ({ installations: [] }),
|
|
79
|
+
});
|
|
80
|
+
const result = await githubApi("/user/installations?per_page=100", fakeAuth);
|
|
81
|
+
expect(result).toEqual({ installations: [] });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
// ─── ~/.hq/app-token.json persistence ──────────────────────────────────────
|
|
85
|
+
describe("App token persistence", () => {
|
|
86
|
+
afterEach(() => {
|
|
87
|
+
// Clean up temp token file between tests
|
|
88
|
+
try {
|
|
89
|
+
fs.unlinkSync(tmpTokenPath);
|
|
90
|
+
}
|
|
91
|
+
catch { }
|
|
92
|
+
});
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
// Clean up the real path if any test accidentally wrote there
|
|
95
|
+
// (shouldn't happen — we test with tmpTokenPath)
|
|
96
|
+
});
|
|
97
|
+
it("HQ_APP_TOKEN_PATH points to ~/.hq/app-token.json", () => {
|
|
98
|
+
const expected = path.join(os.homedir(), ".hq", "app-token.json");
|
|
99
|
+
expect(HQ_APP_TOKEN_PATH).toBe(expected);
|
|
100
|
+
});
|
|
101
|
+
it("saveGitHubAuth writes token file to disk", () => {
|
|
102
|
+
saveGitHubAuth(fakeAuth, tmpTokenPath);
|
|
103
|
+
// File was written
|
|
104
|
+
expect(fs.existsSync(tmpTokenPath)).toBe(true);
|
|
105
|
+
const stored = JSON.parse(fs.readFileSync(tmpTokenPath, "utf-8"));
|
|
106
|
+
expect(stored.login).toBe("testuser");
|
|
107
|
+
expect(stored.access_token).toBe("ghu_fake_app_token");
|
|
108
|
+
});
|
|
109
|
+
it("saveGitHubAuth creates ~/.hq/ directory if missing", () => {
|
|
110
|
+
const nested = path.join(tmpDir, "sub", "app-token.json");
|
|
111
|
+
saveGitHubAuth(fakeAuth, nested);
|
|
112
|
+
expect(fs.existsSync(nested)).toBe(true);
|
|
113
|
+
});
|
|
114
|
+
it("saveGitHubAuth sets restrictive file permissions (0600)", () => {
|
|
115
|
+
saveGitHubAuth(fakeAuth, tmpTokenPath);
|
|
116
|
+
const stat = fs.statSync(tmpTokenPath);
|
|
117
|
+
// Owner read+write only (0600 = 0o600 = 384 decimal)
|
|
118
|
+
expect(stat.mode & 0o777).toBe(0o600);
|
|
119
|
+
});
|
|
120
|
+
it("loadGitHubAuth reads from token file when present", () => {
|
|
121
|
+
// Write a valid token file
|
|
122
|
+
fs.writeFileSync(tmpTokenPath, JSON.stringify(fakeAuth), "utf-8");
|
|
123
|
+
const loaded = loadGitHubAuth(tmpTokenPath);
|
|
124
|
+
expect(loaded).not.toBeNull();
|
|
125
|
+
expect(loaded.login).toBe("testuser");
|
|
126
|
+
expect(loaded.access_token).toBe("ghu_fake_app_token");
|
|
127
|
+
});
|
|
128
|
+
it("loadGitHubAuth returns null when token file does not exist", () => {
|
|
129
|
+
const loaded = loadGitHubAuth(tmpTokenPath);
|
|
130
|
+
expect(loaded).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
it("loadGitHubAuth returns null for corrupted JSON", () => {
|
|
133
|
+
fs.writeFileSync(tmpTokenPath, "NOT VALID JSON{{{", "utf-8");
|
|
134
|
+
const loaded = loadGitHubAuth(tmpTokenPath);
|
|
135
|
+
expect(loaded).toBeNull();
|
|
136
|
+
});
|
|
137
|
+
it("loadGitHubAuth returns null when token file is missing access_token", () => {
|
|
138
|
+
fs.writeFileSync(tmpTokenPath, JSON.stringify({ login: "x", id: 1 }), "utf-8");
|
|
139
|
+
const loaded = loadGitHubAuth(tmpTokenPath);
|
|
140
|
+
expect(loaded).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
it("clearGitHubAuth removes the token file", () => {
|
|
143
|
+
fs.writeFileSync(tmpTokenPath, JSON.stringify(fakeAuth), "utf-8");
|
|
144
|
+
expect(fs.existsSync(tmpTokenPath)).toBe(true);
|
|
145
|
+
clearGitHubAuth(tmpTokenPath);
|
|
146
|
+
expect(fs.existsSync(tmpTokenPath)).toBe(false);
|
|
147
|
+
});
|
|
148
|
+
it("clearGitHubAuth is a no-op when file does not exist", () => {
|
|
149
|
+
// Should not throw
|
|
150
|
+
clearGitHubAuth(tmpTokenPath);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
// ─── isGitHubAuthValid ─────────────────────────────────────────────────────
|
|
154
|
+
describe("isGitHubAuthValid", () => {
|
|
155
|
+
beforeEach(() => mockFetch.mockReset());
|
|
156
|
+
it("returns true when /user responds 200", async () => {
|
|
157
|
+
mockFetch.mockResolvedValueOnce({ ok: true });
|
|
158
|
+
expect(await isGitHubAuthValid(fakeAuth)).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
it("returns false when /user responds non-ok", async () => {
|
|
161
|
+
mockFetch.mockResolvedValueOnce({ ok: false, status: 401 });
|
|
162
|
+
expect(await isGitHubAuthValid(fakeAuth)).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
it("returns false on network error", async () => {
|
|
165
|
+
mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"));
|
|
166
|
+
expect(await isGitHubAuthValid(fakeAuth)).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
// ─── isAppScopedToken ──────────────────────────────────────────────────────
|
|
170
|
+
describe("isAppScopedToken", () => {
|
|
171
|
+
beforeEach(() => mockFetch.mockReset());
|
|
172
|
+
it('returns "yes" when /user/installations responds 200', async () => {
|
|
173
|
+
mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
|
|
174
|
+
expect(await isAppScopedToken(fakeAuth)).toBe("yes");
|
|
175
|
+
});
|
|
176
|
+
it('returns "no" on 403 (definitive — wrong token type)', async () => {
|
|
177
|
+
mockFetch.mockResolvedValueOnce({ ok: false, status: 403 });
|
|
178
|
+
expect(await isAppScopedToken(fakeAuth)).toBe("no");
|
|
179
|
+
});
|
|
180
|
+
it('returns "unknown" on 5xx (transient server error)', async () => {
|
|
181
|
+
mockFetch.mockResolvedValueOnce({ ok: false, status: 500 });
|
|
182
|
+
expect(await isAppScopedToken(fakeAuth)).toBe("unknown");
|
|
183
|
+
});
|
|
184
|
+
it('returns "unknown" on network error', async () => {
|
|
185
|
+
mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"));
|
|
186
|
+
expect(await isAppScopedToken(fakeAuth)).toBe("unknown");
|
|
187
|
+
});
|
|
188
|
+
it('returns "no" when access_token is empty', async () => {
|
|
189
|
+
expect(await isAppScopedToken({ ...fakeAuth, access_token: "" })).toBe("no");
|
|
190
|
+
// fetch should not have been called
|
|
191
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
// ─── Cleanup ───────────────────────────────────────────────────────────────
|
|
195
|
+
afterAll(() => {
|
|
196
|
+
try {
|
|
197
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
198
|
+
}
|
|
199
|
+
catch { }
|
|
200
|
+
});
|
|
201
|
+
//# sourceMappingURL=auth.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.test.js","sourceRoot":"","sources":["../../src/__tests__/auth.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -14,8 +14,11 @@ const PLACEHOLDER_EXEMPT_PATHS = [
|
|
|
14
14
|
"starter-projects",
|
|
15
15
|
".claude/policies",
|
|
16
16
|
".claude/commands",
|
|
17
|
+
".claude/skills",
|
|
18
|
+
".claude/CLAUDE.md",
|
|
17
19
|
"modules/modules.yaml",
|
|
18
20
|
"README.md",
|
|
21
|
+
"USER-GUIDE.md",
|
|
19
22
|
"workers",
|
|
20
23
|
];
|
|
21
24
|
function isExemptFromPlaceholderCheck(relPath) {
|
|
@@ -66,7 +69,17 @@ describe("scaffold integration", () => {
|
|
|
66
69
|
// Run compute-checksums first so integrity check has fresh checksums
|
|
67
70
|
const computeScript = path.join(tmpDir, "scripts", "compute-checksums.sh");
|
|
68
71
|
if (fs.existsSync(computeScript)) {
|
|
69
|
-
|
|
72
|
+
try {
|
|
73
|
+
execSync(`bash "${computeScript}"`, { cwd: tmpDir, stdio: "pipe" });
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
const msg = err?.stderr?.toString() ?? "";
|
|
77
|
+
if (msg.includes("yq is required")) {
|
|
78
|
+
console.log(" Skipping core-integrity check (yq not installed)");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
70
83
|
}
|
|
71
84
|
// execSync throws on non-zero exit code, so reaching next line means success
|
|
72
85
|
execSync(`bash "${script}"`, { cwd: tmpDir, stdio: "pipe" });
|