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.
Files changed (56) hide show
  1. package/commands/invite.md +215 -0
  2. package/commands/promote.md +161 -0
  3. package/commands/team-sync.md +431 -0
  4. package/dist/__tests__/auth.test.d.ts +2 -0
  5. package/dist/__tests__/auth.test.d.ts.map +1 -0
  6. package/dist/__tests__/auth.test.js +201 -0
  7. package/dist/__tests__/auth.test.js.map +1 -0
  8. package/dist/__tests__/scaffold.test.js +14 -1
  9. package/dist/__tests__/scaffold.test.js.map +1 -1
  10. package/dist/admin-onboarding.d.ts.map +1 -1
  11. package/dist/admin-onboarding.js +11 -0
  12. package/dist/admin-onboarding.js.map +1 -1
  13. package/dist/auth.d.ts +58 -13
  14. package/dist/auth.d.ts.map +1 -1
  15. package/dist/auth.js +105 -29
  16. package/dist/auth.js.map +1 -1
  17. package/dist/fetch-template.d.ts +3 -3
  18. package/dist/fetch-template.d.ts.map +1 -1
  19. package/dist/fetch-template.js +93 -71
  20. package/dist/fetch-template.js.map +1 -1
  21. package/dist/git.d.ts +12 -1
  22. package/dist/git.d.ts.map +1 -1
  23. package/dist/git.js +40 -20
  24. package/dist/git.js.map +1 -1
  25. package/dist/index.js +0 -0
  26. package/dist/join-flow.d.ts.map +1 -1
  27. package/dist/join-flow.js +11 -0
  28. package/dist/join-flow.js.map +1 -1
  29. package/dist/scaffold.d.ts.map +1 -1
  30. package/dist/scaffold.js +48 -13
  31. package/dist/scaffold.js.map +1 -1
  32. package/dist/team-setup.d.ts +25 -0
  33. package/dist/team-setup.d.ts.map +1 -1
  34. package/dist/team-setup.js +113 -0
  35. package/dist/team-setup.js.map +1 -1
  36. package/dist/teams-flow.d.ts +7 -2
  37. package/dist/teams-flow.d.ts.map +1 -1
  38. package/dist/teams-flow.js +31 -8
  39. package/dist/teams-flow.js.map +1 -1
  40. package/dist/ui.d.ts +3 -0
  41. package/dist/ui.d.ts.map +1 -1
  42. package/dist/ui.js +79 -3
  43. package/dist/ui.js.map +1 -1
  44. package/package.json +3 -2
  45. package/dist/art.d.ts +0 -17
  46. package/dist/art.d.ts.map +0 -1
  47. package/dist/art.js +0 -171
  48. package/dist/art.js.map +0 -1
  49. package/dist/cloud.d.ts +0 -26
  50. package/dist/cloud.d.ts.map +0 -1
  51. package/dist/cloud.js +0 -126
  52. package/dist/cloud.js.map +0 -1
  53. package/dist/tui.d.ts +0 -8
  54. package/dist/tui.d.ts.map +0 -1
  55. package/dist/tui.js +0 -86
  56. 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=auth.test.d.ts.map
@@ -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
- execSync(`bash "${computeScript}"`, { cwd: tmpDir, stdio: "pipe" });
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" });