@blueprintit/shop-os-install 0.5.1 → 0.5.3
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/bin/shop-os-install.js +168 -19
- package/package.json +1 -1
package/bin/shop-os-install.js
CHANGED
|
@@ -21,6 +21,7 @@ import { createInterface } from "node:readline/promises";
|
|
|
21
21
|
import { stdin, stdout, stderr, exit } from "node:process";
|
|
22
22
|
import { homedir } from "node:os";
|
|
23
23
|
import { join, dirname, resolve } from "node:path";
|
|
24
|
+
import { spawnSync } from "node:child_process";
|
|
24
25
|
import {
|
|
25
26
|
existsSync,
|
|
26
27
|
mkdirSync,
|
|
@@ -139,7 +140,7 @@ async function validateLicense(key) {
|
|
|
139
140
|
const url = `${LICENSE_SERVER}/validate?key=${encodeURIComponent(key)}`;
|
|
140
141
|
let resp;
|
|
141
142
|
try {
|
|
142
|
-
resp = await fetch(url, { headers: { "user-agent": "shop-os-installer/0.5.
|
|
143
|
+
resp = await fetch(url, { headers: { "user-agent": "shop-os-installer/0.5.3" } });
|
|
143
144
|
} catch (e) {
|
|
144
145
|
return { ok: false, error: `network: ${e.message}` };
|
|
145
146
|
}
|
|
@@ -173,14 +174,15 @@ function writeJSON(path, obj) {
|
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
function ensureMarketplaces(claudeRoot) {
|
|
176
|
-
// Always (re-)register marketplaces and
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
177
|
+
// Always (re-)register marketplaces and refresh the on-disk clone to the
|
|
178
|
+
// latest origin/main. Claude Code does NOT auto-pull marketplace clones, so
|
|
179
|
+
// a clone left at an old commit (e.g. the 5/20 rebrand snapshot) keeps
|
|
180
|
+
// serving stale plugins forever. We use git directly to refresh — pull when
|
|
181
|
+
// possible, fall back to wipe-and-re-clone if pull fails or the directory
|
|
182
|
+
// isn't a git repo.
|
|
181
183
|
//
|
|
182
184
|
// We still report `added` vs already-known to keep the customer-facing
|
|
183
|
-
// install output consistent across runs — the
|
|
185
|
+
// install output consistent across runs — the refresh is intentionally
|
|
184
186
|
// invisible.
|
|
185
187
|
const path = join(claudeRoot, "plugins", "known_marketplaces.json");
|
|
186
188
|
const known = readJSON(path, {});
|
|
@@ -193,20 +195,47 @@ function ensureMarketplaces(claudeRoot) {
|
|
|
193
195
|
installLocation,
|
|
194
196
|
lastUpdated: new Date().toISOString(),
|
|
195
197
|
};
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
rmSync(installLocation, { recursive: true, force: true });
|
|
199
|
-
} catch {
|
|
200
|
-
// Best-effort. Locked files on Windows can block removal; in that case
|
|
201
|
-
// Claude Code will keep using the stale clone. Customer can quit Claude
|
|
202
|
-
// Code and re-run to recover.
|
|
203
|
-
}
|
|
204
|
-
}
|
|
198
|
+
refreshMarketplaceClone(mp, installLocation);
|
|
205
199
|
}
|
|
206
200
|
writeJSON(path, known);
|
|
207
201
|
return { added, total: MARKETPLACES.length };
|
|
208
202
|
}
|
|
209
203
|
|
|
204
|
+
function refreshMarketplaceClone(mp, installLocation) {
|
|
205
|
+
// GitHub HTTPS URL — same form Claude Code uses internally.
|
|
206
|
+
const repoUrl = `https://github.com/${mp.source.repo}.git`;
|
|
207
|
+
|
|
208
|
+
// If a git checkout exists, fast-forward it to origin/main.
|
|
209
|
+
if (existsSync(join(installLocation, ".git"))) {
|
|
210
|
+
const fetch = spawnSync("git", ["fetch", "origin", "main", "--depth=1"], {
|
|
211
|
+
cwd: installLocation,
|
|
212
|
+
stdio: "ignore",
|
|
213
|
+
});
|
|
214
|
+
if (fetch.status === 0) {
|
|
215
|
+
const reset = spawnSync("git", ["reset", "--hard", "FETCH_HEAD"], {
|
|
216
|
+
cwd: installLocation,
|
|
217
|
+
stdio: "ignore",
|
|
218
|
+
});
|
|
219
|
+
if (reset.status === 0) return;
|
|
220
|
+
}
|
|
221
|
+
// Fetch/reset failed — fall through to wipe + fresh clone.
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Wipe anything in the install location before cloning fresh. Best-effort:
|
|
225
|
+
// locked files on Windows may block removal, in which case clone below will
|
|
226
|
+
// also fail and the customer falls back to whatever they had.
|
|
227
|
+
if (existsSync(installLocation)) {
|
|
228
|
+
try {
|
|
229
|
+
rmSync(installLocation, { recursive: true, force: true });
|
|
230
|
+
} catch {
|
|
231
|
+
// intentional fall-through
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
mkdirSync(dirname(installLocation), { recursive: true });
|
|
236
|
+
spawnSync("git", ["clone", "--depth=1", repoUrl, installLocation], { stdio: "ignore" });
|
|
237
|
+
}
|
|
238
|
+
|
|
210
239
|
function ensurePluginsInstalled(claudeRoot) {
|
|
211
240
|
// Always reset the Shop OS-required plugin entries to a pending user-scope
|
|
212
241
|
// record. A previously-installed pinned entry (e.g. obsidian 3.8.0 from an
|
|
@@ -229,6 +258,30 @@ function ensurePluginsInstalled(claudeRoot) {
|
|
|
229
258
|
const queued = [];
|
|
230
259
|
for (const id of PLUGINS_TO_ENABLE) {
|
|
231
260
|
if (!existing.plugins[id]) queued.push(id);
|
|
261
|
+
|
|
262
|
+
// Wipe the plugin's per-version cache directory so Claude Code reinstalls
|
|
263
|
+
// from the (just-refreshed) marketplace clone instead of loading a stale
|
|
264
|
+
// pinned version. The cache layout is cache/<marketplace>/<plugin>/<ver>/,
|
|
265
|
+
// so removing the whole /<plugin>/ subtree clears every cached version.
|
|
266
|
+
// Other plugins inside cache/<marketplace>/ are left alone.
|
|
267
|
+
const [pluginName, marketplaceName] = id.split("@");
|
|
268
|
+
if (pluginName && marketplaceName) {
|
|
269
|
+
const pluginCacheDir = join(
|
|
270
|
+
claudeRoot,
|
|
271
|
+
"plugins",
|
|
272
|
+
"cache",
|
|
273
|
+
marketplaceName,
|
|
274
|
+
pluginName,
|
|
275
|
+
);
|
|
276
|
+
if (existsSync(pluginCacheDir)) {
|
|
277
|
+
try {
|
|
278
|
+
rmSync(pluginCacheDir, { recursive: true, force: true });
|
|
279
|
+
} catch {
|
|
280
|
+
// Best-effort: locked file on Windows leaves the cache in place.
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
232
285
|
existing.plugins[id] = [
|
|
233
286
|
{
|
|
234
287
|
scope: "user",
|
|
@@ -244,6 +297,45 @@ function ensurePluginsInstalled(claudeRoot) {
|
|
|
244
297
|
return { queued, total: PLUGINS_TO_ENABLE.length };
|
|
245
298
|
}
|
|
246
299
|
|
|
300
|
+
// Vault-scoped permission allowlist. Non-technical Shop OS customers were hitting
|
|
301
|
+
// constant tool-permission prompts during /bp-setup (~50+ Read/Write/Bash dialogs
|
|
302
|
+
// per onboarding run). These patterns pre-approve the tool surface bp-setup and
|
|
303
|
+
// the daily Shop OS flow actually use, scoped to this vault's project settings —
|
|
304
|
+
// not the user's global settings. Risk model: customer's own paid Claude
|
|
305
|
+
// subscription, own machine, own data; we trade some inbound-injection surface
|
|
306
|
+
// for a workable UX. Writes/edits are intentionally bounded to the vault path.
|
|
307
|
+
function buildPermissionAllowList(vaultPath) {
|
|
308
|
+
return [
|
|
309
|
+
// Read-side: bp-setup reads reference templates from the marketplace clone
|
|
310
|
+
// (~/.claude/plugins/...) AND any context files the user drops in. Read,
|
|
311
|
+
// Glob, and Grep are low-risk; allow them broadly.
|
|
312
|
+
"Read",
|
|
313
|
+
"Glob",
|
|
314
|
+
"Grep",
|
|
315
|
+
// Write-side: scope to the vault directory so Claude can't be coaxed into
|
|
316
|
+
// writing outside it.
|
|
317
|
+
`Write(${vaultPath}/**)`,
|
|
318
|
+
`Edit(${vaultPath}/**)`,
|
|
319
|
+
// Bash: bp-setup creates directories, chmods hooks, finds reference files.
|
|
320
|
+
// Restrict to the specific subcommands the skill actually invokes.
|
|
321
|
+
"Bash(mkdir:*)",
|
|
322
|
+
"Bash(chmod:*)",
|
|
323
|
+
"Bash(find:*)",
|
|
324
|
+
"Bash(ls:*)",
|
|
325
|
+
"Bash(cat:*)",
|
|
326
|
+
"Bash(grep:*)",
|
|
327
|
+
"Bash(echo:*)",
|
|
328
|
+
"Bash(test:*)",
|
|
329
|
+
"Bash(touch:*)",
|
|
330
|
+
// Network: bp-setup's Phase B+ fetches links the user pastes (LinkedIn,
|
|
331
|
+
// About pages, brand guidelines, etc.).
|
|
332
|
+
"WebFetch",
|
|
333
|
+
"WebSearch",
|
|
334
|
+
// Plan tool: surfaced by some Superpowers skills, no need to prompt.
|
|
335
|
+
"TodoWrite",
|
|
336
|
+
];
|
|
337
|
+
}
|
|
338
|
+
|
|
247
339
|
function enableForVault(vaultPath) {
|
|
248
340
|
const settingsPath = join(vaultPath, ".claude", "settings.json");
|
|
249
341
|
const settings = readJSON(settingsPath, {});
|
|
@@ -251,10 +343,57 @@ function enableForVault(vaultPath) {
|
|
|
251
343
|
for (const id of PLUGINS_TO_ENABLE) {
|
|
252
344
|
settings.enabledPlugins[id] = true;
|
|
253
345
|
}
|
|
346
|
+
// Merge (don't clobber) any existing permission allowlist a re-install would
|
|
347
|
+
// have written, then re-add ours. De-dupe by entry string.
|
|
348
|
+
if (!settings.permissions) settings.permissions = {};
|
|
349
|
+
const existing = Array.isArray(settings.permissions.allow)
|
|
350
|
+
? settings.permissions.allow
|
|
351
|
+
: [];
|
|
352
|
+
const ours = buildPermissionAllowList(vaultPath);
|
|
353
|
+
settings.permissions.allow = Array.from(new Set([...existing, ...ours]));
|
|
254
354
|
writeJSON(settingsPath, settings);
|
|
255
355
|
return settingsPath;
|
|
256
356
|
}
|
|
257
357
|
|
|
358
|
+
// Pre-empt Claude Code's first-run wizard (theme picker, terminal-setup prompt,
|
|
359
|
+
// onboarding screens) by writing the "already onboarded" flags BEFORE the
|
|
360
|
+
// setup script launches `claude`. The auth/sign-in flow is independent and
|
|
361
|
+
// still happens — we can't bypass that and don't try. Safe to run repeatedly:
|
|
362
|
+
// merges with any existing config rather than overwriting.
|
|
363
|
+
function seedClaudeCodeDefaults(claudeRoot) {
|
|
364
|
+
const seeded = { onboardingFlags: false, theme: false };
|
|
365
|
+
|
|
366
|
+
// ~/.claude.json holds Claude Code's user-state (numStartups, hasCompletedOnboarding,
|
|
367
|
+
// anonymousId, etc.). Live next to ~/.claude/, not inside it.
|
|
368
|
+
const userStatePath = join(homedir(), ".claude.json");
|
|
369
|
+
const userState = readJSON(userStatePath, {});
|
|
370
|
+
if (!userState.hasCompletedOnboarding) {
|
|
371
|
+
userState.hasCompletedOnboarding = true;
|
|
372
|
+
if (!userState.lastOnboardingVersion) userState.lastOnboardingVersion = "1.0.30";
|
|
373
|
+
seeded.onboardingFlags = true;
|
|
374
|
+
try {
|
|
375
|
+
writeFileSync(userStatePath, JSON.stringify(userState, null, 2) + "\n", "utf8");
|
|
376
|
+
} catch {
|
|
377
|
+
// best-effort; permission errors here aren't worth blocking the install
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// ~/.claude/settings.json holds user-scope settings (theme, permissions, etc.)
|
|
382
|
+
const settingsPath = join(claudeRoot, "settings.json");
|
|
383
|
+
const settings = readJSON(settingsPath, {});
|
|
384
|
+
if (!settings.theme) {
|
|
385
|
+
settings.theme = "dark";
|
|
386
|
+
seeded.theme = true;
|
|
387
|
+
try {
|
|
388
|
+
writeJSON(settingsPath, settings);
|
|
389
|
+
} catch {
|
|
390
|
+
// best-effort
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return seeded;
|
|
395
|
+
}
|
|
396
|
+
|
|
258
397
|
function createVaultClaudeMd(vaultPath, license) {
|
|
259
398
|
const claudeMd = join(vaultPath, "CLAUDE.md");
|
|
260
399
|
if (existsSync(claudeMd)) return false; // do not overwrite an existing vault
|
|
@@ -643,15 +782,25 @@ async function main() {
|
|
|
643
782
|
if (rawResult.created) ok("Raw/ inbox + Raw/processed/ created (drop materials in Raw/ to seed the vault)");
|
|
644
783
|
else info("Raw/ inbox already present (left untouched)");
|
|
645
784
|
|
|
646
|
-
print(dim(" [4/
|
|
785
|
+
print(dim(" [4/7] Enabling plugins + permission allowlist for this vault"));
|
|
647
786
|
const settingsPath = enableForVault(vaultPath);
|
|
648
787
|
ok(`Wrote ${settingsPath.replace(homedir(), "~")}`);
|
|
788
|
+
info("Pre-approved Read/Write/Edit/Bash patterns so /bp-setup runs without permission prompts");
|
|
789
|
+
|
|
790
|
+
print(dim(" [5/7] Pre-seeding Claude Code defaults"));
|
|
791
|
+
const seeded = seedClaudeCodeDefaults(claudeRoot);
|
|
792
|
+
if (seeded.onboardingFlags || seeded.theme) {
|
|
793
|
+
if (seeded.onboardingFlags) ok("Marked Claude Code onboarding complete (skips theme/terminal wizard on first launch)");
|
|
794
|
+
if (seeded.theme) ok("Set Claude Code theme to dark");
|
|
795
|
+
} else {
|
|
796
|
+
info("Claude Code already configured — left existing settings untouched");
|
|
797
|
+
}
|
|
649
798
|
|
|
650
|
-
print(dim(" [
|
|
799
|
+
print(dim(" [6/7] Saving license"));
|
|
651
800
|
const licensePath = saveLicenseFile(license);
|
|
652
801
|
ok(`License saved to ${licensePath.replace(homedir(), "~")} (chmod 600)`);
|
|
653
802
|
|
|
654
|
-
print(dim(" [
|
|
803
|
+
print(dim(" [7/7] Installing Shop OS Chat launcher"));
|
|
655
804
|
const launcherPath = writeChatLauncher(vaultPath);
|
|
656
805
|
ok(`Wrote ${launcherPath.replace(homedir(), "~")}`);
|
|
657
806
|
|