@blueprintit/shop-os-install 0.5.0 → 0.5.2

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.
@@ -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.0" } });
143
+ resp = await fetch(url, { headers: { "user-agent": "shop-os-installer/0.5.2" } });
143
144
  } catch (e) {
144
145
  return { ok: false, error: `network: ${e.message}` };
145
146
  }
@@ -173,52 +174,114 @@ function writeJSON(path, obj) {
173
174
  }
174
175
 
175
176
  function ensureMarketplaces(claudeRoot) {
176
- // Always (re-)register the marketplaces and clear any stale clone on disk.
177
- // A clone pinned to an old commit is the #1 reason re-installs keep serving
178
- // old plugin versions (e.g. obsidian 3.8.0 skills appearing months after
179
- // the marketplace shipped 3.12.0). Clearing the directory forces Claude Code
180
- // to re-clone from origin on its next launch.
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.
183
+ //
184
+ // We still report `added` vs already-known to keep the customer-facing
185
+ // install output consistent across runs — the refresh is intentionally
186
+ // invisible.
181
187
  const path = join(claudeRoot, "plugins", "known_marketplaces.json");
182
188
  const known = readJSON(path, {});
183
- const cleared = [];
189
+ const added = [];
184
190
  for (const mp of MARKETPLACES) {
185
191
  const installLocation = join(claudeRoot, "plugins", "marketplaces", mp.name);
192
+ if (!known[mp.name]) added.push(mp.name);
186
193
  known[mp.name] = {
187
194
  source: { source: mp.source.type, repo: mp.source.repo },
188
195
  installLocation,
189
196
  lastUpdated: new Date().toISOString(),
190
197
  };
191
- if (existsSync(installLocation)) {
192
- try {
193
- rmSync(installLocation, { recursive: true, force: true });
194
- cleared.push(mp.name);
195
- } catch {
196
- // Best-effort. Locked files on Windows can prevent removal; Claude Code
197
- // may keep using the stale clone in that case. Customer can manually
198
- // delete the folder and re-launch to recover.
199
- }
200
- }
198
+ refreshMarketplaceClone(mp, installLocation);
201
199
  }
202
200
  writeJSON(path, known);
203
- return { cleared, total: MARKETPLACES.length };
201
+ return { added, total: MARKETPLACES.length };
202
+ }
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" });
204
237
  }
205
238
 
206
239
  function ensurePluginsInstalled(claudeRoot) {
207
240
  // Always reset the Shop OS-required plugin entries to a pending user-scope
208
- // record. We can't use a presence check here: a previously-installed pinned
209
- // entry (e.g. obsidian 3.8.0 from a prior beta) would prevent Claude Code
210
- // from picking up the marketplace's current version. Forcing pending status
211
- // makes Claude Code re-resolve the plugin against the (just-refreshed)
212
- // marketplace clone on its next launch.
241
+ // record. A previously-installed pinned entry (e.g. obsidian 3.8.0 from an
242
+ // earlier beta) would otherwise prevent Claude Code from picking up the
243
+ // marketplace's current version. Pending status forces Claude Code to
244
+ // re-resolve the plugin against the (just-refreshed) marketplace clone on
245
+ // its next launch.
213
246
  //
214
247
  // This wipes any project-scope variants of these specific plugin ids — that's
215
248
  // intentional. Shop OS is meant to be enabled at user scope so the same
216
249
  // plugin set works across every vault the operator runs.
250
+ //
251
+ // We still report whether entries were newly created vs pre-existing so the
252
+ // customer-facing install output matches the v0.4.0 conventions — the
253
+ // forced-reset is intentionally invisible.
217
254
  const path = join(claudeRoot, "plugins", "installed_plugins.json");
218
255
  const existing = readJSON(path, { version: 2, plugins: {} });
219
256
  if (!existing.plugins) existing.plugins = {};
220
257
  const installedAt = new Date().toISOString();
258
+ const queued = [];
221
259
  for (const id of PLUGINS_TO_ENABLE) {
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
+
222
285
  existing.plugins[id] = [
223
286
  {
224
287
  scope: "user",
@@ -231,7 +294,7 @@ function ensurePluginsInstalled(claudeRoot) {
231
294
  ];
232
295
  }
233
296
  writeJSON(path, existing);
234
- return PLUGINS_TO_ENABLE;
297
+ return { queued, total: PLUGINS_TO_ENABLE.length };
235
298
  }
236
299
 
237
300
  function enableForVault(vaultPath) {
@@ -601,15 +664,20 @@ async function main() {
601
664
 
602
665
  print(dim(" [1/6] Registering plugin marketplaces"));
603
666
  const mpResult = ensureMarketplaces(claudeRoot);
604
- for (const mp of MARKETPLACES) ok(`Marketplace ready: ${mp.name}`);
605
- if (mpResult.cleared.length) {
606
- info(`Cleared stale clone for ${mpResult.cleared.join(", ")} — Claude Code will re-fetch on next launch.`);
667
+ if (mpResult.added.length === 0) {
668
+ info(`All ${mpResult.total} marketplaces already registered`);
669
+ } else {
670
+ for (const name of mpResult.added) ok(`Added marketplace: ${name}`);
607
671
  }
608
672
 
609
- print(dim(" [2/6] Queueing plugins for sync"));
610
- const queued = ensurePluginsInstalled(claudeRoot);
611
- for (const id of queued) ok(`Queued plugin: ${id}`);
612
- info("Claude Code will sync the latest plugin files from the marketplaces on next launch.");
673
+ print(dim(" [2/6] Enabling plugins for installation"));
674
+ const pluginsResult = ensurePluginsInstalled(claudeRoot);
675
+ if (pluginsResult.queued.length === 0) {
676
+ info("All required plugins already queued");
677
+ } else {
678
+ for (const id of pluginsResult.queued) ok(`Queued plugin: ${id}`);
679
+ info("Claude Code will sync the actual plugin files from the marketplaces on next launch.");
680
+ }
613
681
 
614
682
  print(dim(` [3/6] ${isExisting ? "Configuring" : "Creating"} vault at ${vaultPath}`));
615
683
  if (!existsSync(vaultPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blueprintit/shop-os-install",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
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": {