@blueprintit/shop-os-install 0.5.2 → 0.5.4

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.
@@ -119,8 +119,17 @@ function getClaudeRoot() {
119
119
  }
120
120
 
121
121
  function checkClaudeCode() {
122
- const root = getClaudeRoot();
123
- if (!existsSync(root)) {
122
+ // Detect by binary on PATH, not by the ~/.claude directory: when Claude Code
123
+ // is installed via `npm install -g @anthropic-ai/claude-code` (which the
124
+ // setup scripts now do), the .claude directory isn't created until the user
125
+ // launches `claude` for the first time. A binary check correctly identifies
126
+ // installs from npm, the official .ps1/.sh installer, or the desktop app.
127
+ const probe = spawnSync(
128
+ process.platform === "win32" ? "where" : "which",
129
+ ["claude"],
130
+ { stdio: "ignore", shell: false },
131
+ );
132
+ if (probe.status !== 0) {
124
133
  print("");
125
134
  print(red("Claude Code is not installed."));
126
135
  print("");
@@ -131,6 +140,12 @@ function checkClaudeCode() {
131
140
  print("re-run this installer.");
132
141
  exit(1);
133
142
  }
143
+ // Stage ~/.claude so downstream marketplace and plugin writes succeed even
144
+ // if the user hasn't launched `claude` yet to seed the dir themselves.
145
+ const root = getClaudeRoot();
146
+ if (!existsSync(root)) {
147
+ mkdirSync(root, { recursive: true });
148
+ }
134
149
  return root;
135
150
  }
136
151
 
@@ -140,7 +155,7 @@ async function validateLicense(key) {
140
155
  const url = `${LICENSE_SERVER}/validate?key=${encodeURIComponent(key)}`;
141
156
  let resp;
142
157
  try {
143
- resp = await fetch(url, { headers: { "user-agent": "shop-os-installer/0.5.2" } });
158
+ resp = await fetch(url, { headers: { "user-agent": "shop-os-installer/0.5.3" } });
144
159
  } catch (e) {
145
160
  return { ok: false, error: `network: ${e.message}` };
146
161
  }
@@ -297,6 +312,45 @@ function ensurePluginsInstalled(claudeRoot) {
297
312
  return { queued, total: PLUGINS_TO_ENABLE.length };
298
313
  }
299
314
 
315
+ // Vault-scoped permission allowlist. Non-technical Shop OS customers were hitting
316
+ // constant tool-permission prompts during /bp-setup (~50+ Read/Write/Bash dialogs
317
+ // per onboarding run). These patterns pre-approve the tool surface bp-setup and
318
+ // the daily Shop OS flow actually use, scoped to this vault's project settings —
319
+ // not the user's global settings. Risk model: customer's own paid Claude
320
+ // subscription, own machine, own data; we trade some inbound-injection surface
321
+ // for a workable UX. Writes/edits are intentionally bounded to the vault path.
322
+ function buildPermissionAllowList(vaultPath) {
323
+ return [
324
+ // Read-side: bp-setup reads reference templates from the marketplace clone
325
+ // (~/.claude/plugins/...) AND any context files the user drops in. Read,
326
+ // Glob, and Grep are low-risk; allow them broadly.
327
+ "Read",
328
+ "Glob",
329
+ "Grep",
330
+ // Write-side: scope to the vault directory so Claude can't be coaxed into
331
+ // writing outside it.
332
+ `Write(${vaultPath}/**)`,
333
+ `Edit(${vaultPath}/**)`,
334
+ // Bash: bp-setup creates directories, chmods hooks, finds reference files.
335
+ // Restrict to the specific subcommands the skill actually invokes.
336
+ "Bash(mkdir:*)",
337
+ "Bash(chmod:*)",
338
+ "Bash(find:*)",
339
+ "Bash(ls:*)",
340
+ "Bash(cat:*)",
341
+ "Bash(grep:*)",
342
+ "Bash(echo:*)",
343
+ "Bash(test:*)",
344
+ "Bash(touch:*)",
345
+ // Network: bp-setup's Phase B+ fetches links the user pastes (LinkedIn,
346
+ // About pages, brand guidelines, etc.).
347
+ "WebFetch",
348
+ "WebSearch",
349
+ // Plan tool: surfaced by some Superpowers skills, no need to prompt.
350
+ "TodoWrite",
351
+ ];
352
+ }
353
+
300
354
  function enableForVault(vaultPath) {
301
355
  const settingsPath = join(vaultPath, ".claude", "settings.json");
302
356
  const settings = readJSON(settingsPath, {});
@@ -304,10 +358,57 @@ function enableForVault(vaultPath) {
304
358
  for (const id of PLUGINS_TO_ENABLE) {
305
359
  settings.enabledPlugins[id] = true;
306
360
  }
361
+ // Merge (don't clobber) any existing permission allowlist a re-install would
362
+ // have written, then re-add ours. De-dupe by entry string.
363
+ if (!settings.permissions) settings.permissions = {};
364
+ const existing = Array.isArray(settings.permissions.allow)
365
+ ? settings.permissions.allow
366
+ : [];
367
+ const ours = buildPermissionAllowList(vaultPath);
368
+ settings.permissions.allow = Array.from(new Set([...existing, ...ours]));
307
369
  writeJSON(settingsPath, settings);
308
370
  return settingsPath;
309
371
  }
310
372
 
373
+ // Pre-empt Claude Code's first-run wizard (theme picker, terminal-setup prompt,
374
+ // onboarding screens) by writing the "already onboarded" flags BEFORE the
375
+ // setup script launches `claude`. The auth/sign-in flow is independent and
376
+ // still happens — we can't bypass that and don't try. Safe to run repeatedly:
377
+ // merges with any existing config rather than overwriting.
378
+ function seedClaudeCodeDefaults(claudeRoot) {
379
+ const seeded = { onboardingFlags: false, theme: false };
380
+
381
+ // ~/.claude.json holds Claude Code's user-state (numStartups, hasCompletedOnboarding,
382
+ // anonymousId, etc.). Live next to ~/.claude/, not inside it.
383
+ const userStatePath = join(homedir(), ".claude.json");
384
+ const userState = readJSON(userStatePath, {});
385
+ if (!userState.hasCompletedOnboarding) {
386
+ userState.hasCompletedOnboarding = true;
387
+ if (!userState.lastOnboardingVersion) userState.lastOnboardingVersion = "1.0.30";
388
+ seeded.onboardingFlags = true;
389
+ try {
390
+ writeFileSync(userStatePath, JSON.stringify(userState, null, 2) + "\n", "utf8");
391
+ } catch {
392
+ // best-effort; permission errors here aren't worth blocking the install
393
+ }
394
+ }
395
+
396
+ // ~/.claude/settings.json holds user-scope settings (theme, permissions, etc.)
397
+ const settingsPath = join(claudeRoot, "settings.json");
398
+ const settings = readJSON(settingsPath, {});
399
+ if (!settings.theme) {
400
+ settings.theme = "dark";
401
+ seeded.theme = true;
402
+ try {
403
+ writeJSON(settingsPath, settings);
404
+ } catch {
405
+ // best-effort
406
+ }
407
+ }
408
+
409
+ return seeded;
410
+ }
411
+
311
412
  function createVaultClaudeMd(vaultPath, license) {
312
413
  const claudeMd = join(vaultPath, "CLAUDE.md");
313
414
  if (existsSync(claudeMd)) return false; // do not overwrite an existing vault
@@ -696,15 +797,25 @@ async function main() {
696
797
  if (rawResult.created) ok("Raw/ inbox + Raw/processed/ created (drop materials in Raw/ to seed the vault)");
697
798
  else info("Raw/ inbox already present (left untouched)");
698
799
 
699
- print(dim(" [4/6] Enabling plugins for this vault"));
800
+ print(dim(" [4/7] Enabling plugins + permission allowlist for this vault"));
700
801
  const settingsPath = enableForVault(vaultPath);
701
802
  ok(`Wrote ${settingsPath.replace(homedir(), "~")}`);
803
+ info("Pre-approved Read/Write/Edit/Bash patterns so /bp-setup runs without permission prompts");
804
+
805
+ print(dim(" [5/7] Pre-seeding Claude Code defaults"));
806
+ const seeded = seedClaudeCodeDefaults(claudeRoot);
807
+ if (seeded.onboardingFlags || seeded.theme) {
808
+ if (seeded.onboardingFlags) ok("Marked Claude Code onboarding complete (skips theme/terminal wizard on first launch)");
809
+ if (seeded.theme) ok("Set Claude Code theme to dark");
810
+ } else {
811
+ info("Claude Code already configured — left existing settings untouched");
812
+ }
702
813
 
703
- print(dim(" [5/6] Saving license"));
814
+ print(dim(" [6/7] Saving license"));
704
815
  const licensePath = saveLicenseFile(license);
705
816
  ok(`License saved to ${licensePath.replace(homedir(), "~")} (chmod 600)`);
706
817
 
707
- print(dim(" [6/6] Installing Shop OS Chat launcher"));
818
+ print(dim(" [7/7] Installing Shop OS Chat launcher"));
708
819
  const launcherPath = writeChatLauncher(vaultPath);
709
820
  ok(`Wrote ${launcherPath.replace(homedir(), "~")}`);
710
821
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueprintit/shop-os-install",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "One-command installer for Shop OS — Blueprint IT's AI Operating System for small businesses.",
5
5
  "type": "module",
6
6
  "bin": {