@blueprintit/shop-os-install 0.3.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/README.md ADDED
@@ -0,0 +1,174 @@
1
+ # @blueprintit/shop-os-install
2
+
3
+ One-command installer for **Shop OS**: Blueprint IT's AI Operating System for small businesses.
4
+
5
+ ## Quick Install (Recommended)
6
+
7
+ Paste **one** of these commands. Everything installs automatically:
8
+
9
+ **Mac** (in Terminal):
10
+ ```sh
11
+ curl -fsSL https://raw.githubusercontent.com/blueprintit-ai/shop-os-installer/main/scripts/setup-macos.sh | bash
12
+ ```
13
+
14
+ **Windows** (in PowerShell as Administrator):
15
+ ```powershell
16
+ irm https://raw.githubusercontent.com/blueprintit-ai/shop-os-installer/main/scripts/setup-windows.ps1 | iex
17
+ ```
18
+
19
+ This installs Node.js, Claude Code, Obsidian, and Shop OS in one go. You'll be prompted for your license key partway through.
20
+
21
+ ---
22
+
23
+ ## Manual Install
24
+
25
+ If you prefer to install prerequisites yourself, run:
26
+
27
+ ```sh
28
+ npx @blueprintit/shop-os-install
29
+ ```
30
+
31
+ The installer (run directly or via the setup scripts above):
32
+
33
+ 1. **Pre-flight**: verifies Node 18+ and Claude Code is installed
34
+ 2. **License validation**: prompts for the license key, validates against `https://shop-os-license-server.glenn-15d.workers.dev/validate`
35
+ 3. **Marketplaces**: registers `blueprintit-ai/blueprint-skills` and `anthropics/claude-plugins-official` in `~/.claude/plugins/known_marketplaces.json`
36
+ 4. **Plugins**: queues `obsidian@blueprint-skills` and `superpowers@claude-plugins-official` in `~/.claude/plugins/installed_plugins.json` (Claude Code does the actual fetch on next launch)
37
+ 5. **Vault**: creates a Shop OS vault folder (default `~/Shop OS Vault`) with a starter `CLAUDE.md`
38
+ 6. **Per-vault config**: writes `<vault>/.claude/settings.json` with `enabledPlugins` set for obsidian + superpowers
39
+ 7. **License record**: saves `~/.shopos/license.json` (chmod 600) for downstream skill validation
40
+ 8. **Next steps**: prints `cd` command and the `/obsidian:os-setup` slash command to run
41
+
42
+ Zero npm dependencies. Uses only Node 18+ built-ins (`fetch`, `readline`, `fs`).
43
+
44
+ ## Customer-facing install email template
45
+
46
+ ```
47
+ Subject: Welcome to Shop OS: your license key inside
48
+
49
+ Hi [Customer],
50
+
51
+ Welcome to Shop OS. Your license key is:
52
+
53
+ SHOP-XXXX-YYYY-ZZZZ
54
+
55
+ To install, open Terminal (Mac) or PowerShell (Windows) and paste this one command:
56
+
57
+ Mac (Terminal):
58
+ curl -fsSL https://raw.githubusercontent.com/blueprintit-ai/shop-os-installer/main/scripts/setup-macos.sh | bash
59
+
60
+ Windows (PowerShell, run as Administrator):
61
+ irm https://raw.githubusercontent.com/blueprintit-ai/shop-os-installer/main/scripts/setup-windows.ps1 | iex
62
+
63
+ The script installs everything (Node.js, Claude Code, Obsidian, and Shop OS) automatically.
64
+ When prompted, paste your license key above. Total time: ~5 minutes depending on your internet speed.
65
+
66
+ Need help? Reply to this email.
67
+
68
+ Blueprint IT
69
+ ```
70
+
71
+ <span style="background-color:#F4EFE3; color:#020309; padding:2px 8px; border-radius:3px; font-size:0.85em;">🤖 Blueprint IT Vault Operator, last edited: 2026-05-26T14:08:59Z</span>
72
+
73
+ (The admin dashboard at `/admin` generates this template per-customer on the fly.)
74
+
75
+ ## Local development
76
+
77
+ ```sh
78
+ # Test the installer against the live license server with a test key
79
+ # (issue one from the admin dashboard first):
80
+ node bin/shop-os-install.js
81
+ ```
82
+
83
+ The script reads from stdin so you can also drive it via heredoc for testing:
84
+
85
+ ```sh
86
+ printf 'SHOP-XXXX-YYYY-ZZZZ\n/tmp/test-vault\ny\n' | node bin/shop-os-install.js
87
+ ```
88
+
89
+ ## Publishing to npm
90
+
91
+ The package is scoped under `@blueprintit`. You need an npm org named `blueprintit` (or change the scope to your personal username).
92
+
93
+ ```sh
94
+ # One-time: create the @blueprintit npm org if it doesn't exist
95
+ # - go to https://www.npmjs.com/org/create
96
+ # - choose Free plan (limited to public packages, fine for an installer)
97
+
98
+ # One-time: log in to npm
99
+ npm login
100
+
101
+ # Publish
102
+ cd "Projects/shop-os-installer"
103
+ npm publish --access public
104
+ ```
105
+
106
+ After publish, the install command works for any customer worldwide:
107
+
108
+ ```sh
109
+ npx @blueprintit/shop-os-install
110
+ ```
111
+
112
+ `npx` always fetches the latest published version, so customers get bug fixes automatically.
113
+
114
+ ## Versioning
115
+
116
+ Bump `version` in `package.json` before each publish:
117
+
118
+ - Patch (`0.1.0` → `0.1.1`): bug fixes
119
+ - Minor (`0.1.0` → `0.2.0`): new install steps or behavior changes
120
+ - Major (`0.1.0` → `1.0.0`): breaking changes (e.g. license server URL change)
121
+
122
+ Then:
123
+
124
+ ```sh
125
+ npm publish --access public
126
+ ```
127
+
128
+ ## Files
129
+
130
+ ```
131
+ shop-os-installer/
132
+ ├── package.json
133
+ ├── README.md (this file)
134
+ ├── .gitignore
135
+ └── bin/
136
+ └── shop-os-install.js (~380 lines, single-file installer)
137
+ ```
138
+
139
+ ## Architecture notes
140
+
141
+ ### Why no dependencies
142
+
143
+ Every transitive dependency in `npx` is fetched fresh each run. Heavy deps make the install feel slow. Node 18+ ships `fetch`, `readline/promises`, and `fs/promises`, which is everything we need. The whole installer downloads in well under a second.
144
+
145
+ ### Why we write to Claude Code config files directly
146
+
147
+ The alternative was to spawn `claude plugin marketplace add ...` and `claude plugin install ...` subprocesses. We chose direct file writes because:
148
+
149
+ - The config file formats are well-known and stable
150
+ - Direct writes are atomic and deterministic
151
+ - We don't have to depend on the `claude` CLI being on the customer's PATH
152
+ - The customer's next Claude Code session will sync the marketplaces and fetch the actual plugin files, the same as if they'd run the commands
153
+
154
+ If a customer's `claude` CLI is broken (PATH issues, version mismatch), our installer still works. We just stage the right config and let Claude Code finish the job.
155
+
156
+ ### What we never touch
157
+
158
+ We never modify:
159
+
160
+ - The customer's existing `enabledPlugins` for other projects (we only write per-vault settings)
161
+ - Other entries in `installed_plugins.json` (we merge, never replace)
162
+ - Other entries in `known_marketplaces.json` (we merge, never replace)
163
+ - Anthropic API keys, Claude Code auth, or any subscription/billing config
164
+
165
+ ## Future enhancements (post-MVP)
166
+
167
+ | Feature | Why |
168
+ |---|---|
169
+ | `--vault <path>` flag | Skip the prompt for scripted installs |
170
+ | `--license <key>` flag | Same, for testing or scripted reinstalls |
171
+ | Update detection | Tell the customer if a newer Shop OS version is available |
172
+ | Telemetry opt-in | Phone home install success/failure counts (anonymous) for product analytics |
173
+ | Uninstall command | `npx @blueprintit/shop-os-uninstall` |
174
+ | Multi-vault mode | Add Shop OS to an existing vault rather than creating a new one |
@@ -0,0 +1,652 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Shop OS Foundation installer.
4
+ *
5
+ * Single command flow for a paying customer:
6
+ * 1. Welcome + pre-flight (Node version, Claude Code present)
7
+ * 2. License key prompt
8
+ * 3. Validate against the live license server
9
+ * 4. Choose vault location
10
+ * 5. Add marketplaces (blueprint-skills + claude-plugins-official)
11
+ * 6. Install plugins (obsidian + superpowers)
12
+ * 7. Create vault directory
13
+ * 8. Enable plugins in <vault>/.claude/settings.json
14
+ * 9. Save license metadata to ~/.shopos/license.json
15
+ * 10. Print next steps
16
+ *
17
+ * Zero npm dependencies. Uses Node 18+ built-ins only.
18
+ */
19
+
20
+ import { createInterface } from "node:readline/promises";
21
+ import { stdin, stdout, stderr, exit } from "node:process";
22
+ import { homedir } from "node:os";
23
+ import { join, dirname, resolve } from "node:path";
24
+ import {
25
+ existsSync,
26
+ mkdirSync,
27
+ readFileSync,
28
+ writeFileSync,
29
+ chmodSync,
30
+ } from "node:fs";
31
+
32
+ const LICENSE_SERVER = "https://shop-os-license-server.glenn-15d.workers.dev";
33
+ const SUPPORT_URL = "https://blueprintit.ai/shop-os/support";
34
+ const DOCS_URL = "https://blueprintit.ai/shop-os/docs";
35
+
36
+ const MARKETPLACES = [
37
+ {
38
+ name: "blueprint-skills",
39
+ source: { type: "github", repo: "blueprintit-ai/blueprint-skills" },
40
+ },
41
+ {
42
+ name: "claude-plugins-official",
43
+ source: { type: "github", repo: "anthropics/claude-plugins-official" },
44
+ },
45
+ ];
46
+
47
+ const PLUGINS_TO_ENABLE = [
48
+ "obsidian@blueprint-skills",
49
+ "superpowers@claude-plugins-official",
50
+ ];
51
+
52
+ // ---------- output helpers ----------
53
+
54
+ const SUPPORTS_COLOR = stdout.isTTY && !process.env.NO_COLOR;
55
+ const c = (code, s) => (SUPPORTS_COLOR ? `\x1b[${code}m${s}\x1b[0m` : s);
56
+ const dim = (s) => c("2", s);
57
+ const bold = (s) => c("1", s);
58
+ const green = (s) => c("32", s);
59
+ const yellow = (s) => c("33", s);
60
+ const red = (s) => c("31", s);
61
+ const cyan = (s) => c("36", s);
62
+
63
+ const print = (msg = "") => stdout.write(msg + "\n");
64
+ const warn = (msg) => stderr.write(yellow("! ") + msg + "\n");
65
+ const fail = (msg) => {
66
+ stderr.write(red("✗ ") + msg + "\n");
67
+ exit(1);
68
+ };
69
+ const ok = (msg) => print(" " + green("✓") + " " + msg);
70
+ const info = (msg) => print(" " + dim("·") + " " + msg);
71
+
72
+ function banner() {
73
+ const lines = [
74
+ "",
75
+ bold(" ╔════════════════════════════════════════════════════════════╗"),
76
+ bold(" ║ ║"),
77
+ bold(" ║ ") + cyan("Shop OS Foundation Installer") + bold(" ║"),
78
+ bold(" ║ ") + dim("AI Operating System for Small Businesses") + bold(" ║"),
79
+ bold(" ║ ║"),
80
+ bold(" ╚════════════════════════════════════════════════════════════╝"),
81
+ "",
82
+ ];
83
+ lines.forEach((l) => print(l));
84
+ }
85
+
86
+ // ---------- prompts ----------
87
+
88
+ async function ask(rl, question, { default: dflt } = {}) {
89
+ const prompt = dflt
90
+ ? `${cyan("?")} ${question} ${dim(`[${dflt}]`)}: `
91
+ : `${cyan("?")} ${question}: `;
92
+ const ans = (await rl.question(prompt)).trim();
93
+ return ans || dflt || "";
94
+ }
95
+
96
+ async function confirm(rl, question, { default: dflt = true } = {}) {
97
+ const hint = dflt ? "Y/n" : "y/N";
98
+ const ans = (await rl.question(`${cyan("?")} ${question} ${dim(`[${hint}]`)}: `))
99
+ .trim()
100
+ .toLowerCase();
101
+ if (!ans) return dflt;
102
+ return ans === "y" || ans === "yes";
103
+ }
104
+
105
+ // ---------- preflight ----------
106
+
107
+ function checkNode() {
108
+ const major = Number(process.versions.node.split(".")[0]);
109
+ if (major < 18) {
110
+ fail(`Node.js 18+ required. You have ${process.version}.`);
111
+ }
112
+ return process.version;
113
+ }
114
+
115
+ function getClaudeRoot() {
116
+ return join(homedir(), ".claude");
117
+ }
118
+
119
+ function checkClaudeCode() {
120
+ const root = getClaudeRoot();
121
+ if (!existsSync(root)) {
122
+ print("");
123
+ print(red("Claude Code is not installed."));
124
+ print("");
125
+ print("Shop OS runs on top of Claude Code. Install it first at:");
126
+ print(" " + cyan("https://claude.ai/code"));
127
+ print("");
128
+ print("Once Claude Code is installed and you have signed in once,");
129
+ print("re-run this installer.");
130
+ exit(1);
131
+ }
132
+ return root;
133
+ }
134
+
135
+ // ---------- license validation ----------
136
+
137
+ async function validateLicense(key) {
138
+ const url = `${LICENSE_SERVER}/validate?key=${encodeURIComponent(key)}`;
139
+ let resp;
140
+ try {
141
+ resp = await fetch(url, { headers: { "user-agent": "shop-os-installer/0.3.0" } });
142
+ } catch (e) {
143
+ return { ok: false, error: `network: ${e.message}` };
144
+ }
145
+ const text = await resp.text();
146
+ let body;
147
+ try {
148
+ body = JSON.parse(text);
149
+ } catch {
150
+ return { ok: false, error: `unexpected response (HTTP ${resp.status})` };
151
+ }
152
+ if (!resp.ok) {
153
+ return { ok: false, error: body.error || `HTTP ${resp.status}` };
154
+ }
155
+ return { ok: true, license: body };
156
+ }
157
+
158
+ // ---------- claude code config ----------
159
+
160
+ function readJSON(path, fallback) {
161
+ if (!existsSync(path)) return fallback;
162
+ try {
163
+ return JSON.parse(readFileSync(path, "utf8"));
164
+ } catch {
165
+ return fallback;
166
+ }
167
+ }
168
+
169
+ function writeJSON(path, obj) {
170
+ mkdirSync(dirname(path), { recursive: true });
171
+ writeFileSync(path, JSON.stringify(obj, null, 2) + "\n", "utf8");
172
+ }
173
+
174
+ function ensureMarketplaces(claudeRoot) {
175
+ const path = join(claudeRoot, "plugins", "known_marketplaces.json");
176
+ const known = readJSON(path, {});
177
+ const added = [];
178
+ for (const mp of MARKETPLACES) {
179
+ if (!known[mp.name]) {
180
+ known[mp.name] = {
181
+ source: { source: mp.source.type, repo: mp.source.repo },
182
+ installLocation: join(claudeRoot, "plugins", "marketplaces", mp.name),
183
+ lastUpdated: new Date().toISOString(),
184
+ };
185
+ added.push(mp.name);
186
+ }
187
+ }
188
+ if (added.length) writeJSON(path, known);
189
+ return { added, total: MARKETPLACES.length };
190
+ }
191
+
192
+ function ensurePluginsInstalled(claudeRoot) {
193
+ // We record the plugins in installed_plugins.json. Claude Code's marketplace
194
+ // refresh on next launch will fetch the actual plugin files into cache/.
195
+ // If they're already present, we leave the entry alone (don't downgrade).
196
+ const path = join(claudeRoot, "plugins", "installed_plugins.json");
197
+ const existing = readJSON(path, { version: 2, plugins: {} });
198
+ if (!existing.plugins) existing.plugins = {};
199
+ const installedAt = new Date().toISOString();
200
+ let changed = false;
201
+ for (const id of PLUGINS_TO_ENABLE) {
202
+ if (!existing.plugins[id]) {
203
+ existing.plugins[id] = [
204
+ {
205
+ scope: "user",
206
+ installPath: null, // filled in by Claude Code on next marketplace sync
207
+ version: "pending",
208
+ installedAt,
209
+ lastUpdated: installedAt,
210
+ gitCommitSha: "pending-sync",
211
+ },
212
+ ];
213
+ changed = true;
214
+ }
215
+ }
216
+ if (changed) writeJSON(path, existing);
217
+ return changed;
218
+ }
219
+
220
+ function enableForVault(vaultPath) {
221
+ const settingsPath = join(vaultPath, ".claude", "settings.json");
222
+ const settings = readJSON(settingsPath, {});
223
+ if (!settings.enabledPlugins) settings.enabledPlugins = {};
224
+ for (const id of PLUGINS_TO_ENABLE) {
225
+ settings.enabledPlugins[id] = true;
226
+ }
227
+ writeJSON(settingsPath, settings);
228
+ return settingsPath;
229
+ }
230
+
231
+ function createVaultClaudeMd(vaultPath, license) {
232
+ const claudeMd = join(vaultPath, "CLAUDE.md");
233
+ if (existsSync(claudeMd)) return false; // do not overwrite an existing vault
234
+ const content = `---
235
+ os-mode: business
236
+ license-customer: ${license.customer}
237
+ license-product: ${license.product}
238
+ installed-at: ${new Date().toISOString()}
239
+ ---
240
+
241
+ # Shop OS Vault
242
+
243
+ Welcome to your Shop OS vault. This is the operating system Blueprint IT installed for ${license.customer}.
244
+
245
+ To finish onboarding, run the following slash command inside Claude Code:
246
+
247
+ \`/obsidian:os-setup\`
248
+
249
+ This walks you through personalizing the vault for your shop: name, owner, key staff,
250
+ services, daily routines, and more.
251
+
252
+ For help, see ${DOCS_URL}
253
+ or reply to your welcome email.
254
+ `;
255
+ mkdirSync(vaultPath, { recursive: true });
256
+ writeFileSync(claudeMd, content, "utf8");
257
+ return true;
258
+ }
259
+
260
+ function createRawInbox(vaultPath) {
261
+ // Create a flat Raw/ inbox with a processed/ subfolder for after-digest moves.
262
+ // Customers drop any raw materials in Raw/ — no subfolders to think about.
263
+ // Claude Code reads, classifies, routes into the vault, and moves the
264
+ // source file to Raw/processed/.
265
+ const rawDir = join(vaultPath, "Raw");
266
+ const processedDir = join(rawDir, "processed");
267
+ const readmePath = join(rawDir, "README.md");
268
+
269
+ const existed = existsSync(rawDir);
270
+ mkdirSync(processedDir, { recursive: true });
271
+
272
+ if (existsSync(readmePath)) return { created: false };
273
+
274
+ const readme = `---
275
+ type: inbox-readme
276
+ tags: [shop-os, inbox, raw]
277
+ ---
278
+
279
+ # Raw / Inbox
280
+
281
+ Drop any raw materials here that you want Shop OS to read and route into your vault.
282
+ PDFs, photos, transcripts, contracts, price lists, spreadsheets, scans, anything.
283
+
284
+ You do NOT need to organize them into subfolders. Just drop them flat. Claude Code
285
+ reads each file, decides where it belongs in the vault, writes a summary into the
286
+ appropriate folder, and moves the original to \`Raw/processed/\` so the inbox stays clean.
287
+
288
+ ## How to trigger a digest
289
+
290
+ Open Claude Code in this vault and type the slash command:
291
+
292
+ \`\`\`
293
+ /os-digest
294
+ \`\`\`
295
+
296
+ One command, easy to remember. Claude does the rest: reads each file, classifies it,
297
+ files the note in the right vault folder, archives the original, and reports back.
298
+ You review the report and the inbox is empty again.
299
+
300
+ ## Examples of what to drop here
301
+
302
+ - A supplier PDF price list
303
+ - A signed customer contract or quote
304
+ - Photos of a completed job
305
+ - A transcript of a sales call (text file or audio)
306
+ - A staff training document
307
+ - Old paper records you scanned
308
+ - Spreadsheets, web pages saved as PDF, anything else
309
+
310
+ The more you drop, the more your vault knows about your shop.
311
+ `;
312
+ writeFileSync(readmePath, readme, "utf8");
313
+ return { created: true, alreadyExisted: existed };
314
+ }
315
+
316
+ function writeChatLauncher(vaultPath) {
317
+ const isWindows = process.platform === "win32";
318
+ const filename = isWindows ? "Shop OS Chat.bat" : "Shop OS Chat.command";
319
+ const filePath = join(vaultPath, filename);
320
+
321
+ let body;
322
+ if (isWindows) {
323
+ body = `@echo off
324
+ setlocal
325
+ set "VAULT_PATH=%~dp0"
326
+ :: Strip trailing backslash
327
+ if "%VAULT_PATH:~-1%"=="\\" set "VAULT_PATH=%VAULT_PATH:~0,-1%"
328
+ echo Starting Shop OS Chat for "%VAULT_PATH%" ...
329
+ npx -y --package=github:blueprintit-ai/shop-os-chat shop-os-chat "%VAULT_PATH%"
330
+ pause
331
+ `;
332
+ } else {
333
+ body = `#!/bin/bash
334
+ # Shop OS Chat launcher — double-click to start.
335
+ VAULT_PATH="$(cd "$(dirname "$0")" && pwd)"
336
+ echo "Starting Shop OS Chat for: $VAULT_PATH"
337
+ npx -y --package=github:blueprintit-ai/shop-os-chat shop-os-chat "$VAULT_PATH"
338
+ echo ""
339
+ echo "Shop OS Chat stopped. You can close this window."
340
+ read -n 1 -s -r -p ""
341
+ `;
342
+ }
343
+ writeFileSync(filePath, body, "utf8");
344
+ if (!isWindows) {
345
+ try { chmodSync(filePath, 0o755); } catch { /* ignore */ }
346
+ }
347
+ return filePath;
348
+ }
349
+
350
+ function expandTilde(p) {
351
+ if (!p) return p;
352
+ if (p === "~") return homedir();
353
+ if (p.startsWith("~/") || p.startsWith("~\\")) return join(homedir(), p.slice(2));
354
+ return p;
355
+ }
356
+
357
+ // Clean a path that came from drag-and-drop or "Copy as path" / "Copy as Pathname".
358
+ // Mac Terminal drag: backslash-escaped spaces and special chars: /Users/foo/Shop\ OS\ Vault
359
+ // Windows "Copy as path": wraps in double quotes: "C:\Users\foo\Shop OS Vault"
360
+ // Mac "Copy as Pathname": no escaping: /Users/foo/Shop OS Vault
361
+ function unwrapShellPath(p) {
362
+ if (!p) return p;
363
+ let s = p.trim();
364
+ const wasQuoted =
365
+ (s.startsWith('"') && s.endsWith('"')) ||
366
+ (s.startsWith("'") && s.endsWith("'"));
367
+ if (wasQuoted) {
368
+ // Windows "Copy as path" and PowerShell drag wrap in quotes — keep backslashes
369
+ // as directory separators.
370
+ s = s.slice(1, -1);
371
+ } else {
372
+ // Mac Terminal drag uses backslash to escape spaces and special chars.
373
+ s = s.replace(/\\(.)/g, "$1");
374
+ }
375
+ return s.trim();
376
+ }
377
+
378
+ function detectSyncFolders() {
379
+ const home = homedir();
380
+ return [
381
+ { name: "Dropbox", path: join(home, "Dropbox") },
382
+ { name: "iCloud Drive", path: join(home, "Library/Mobile Documents/com~apple~CloudDocs") },
383
+ { name: "OneDrive", path: join(home, "OneDrive") },
384
+ ].filter((f) => existsSync(f.path));
385
+ }
386
+
387
+ function printVaultLocationGuide() {
388
+ print(bold("Step 1 of 2: create your vault folder"));
389
+ print("");
390
+ print(" Open Finder (Mac) or File Explorer (Windows).");
391
+ print(" Right-click in the location where you want your vault, choose");
392
+ print(" " + bold("New Folder") + ", and name it " + cyan("Shop OS Vault") + ".");
393
+ print("");
394
+ print(" " + bold("Where to put it:"));
395
+ print(" " + cyan("Single computer") + " -> your home folder or Desktop");
396
+ print(" " + cyan("Multiple machines") + " -> inside Dropbox, iCloud Drive,");
397
+ print(" or OneDrive (any computer signed in to");
398
+ print(" the same account will see the same vault)");
399
+ print("");
400
+ print(" " + dim("Disk space: under 50 MB on day one, 2-5 GB after a year of"));
401
+ print(" " + dim("heavy use. Make sure the drive has 10 GB free."));
402
+ print("");
403
+ print(bold("Step 2 of 2: tell the installer where it is"));
404
+ print("");
405
+ print(" When the prompt below appears, " + bold("drag the folder you just created"));
406
+ print(" " + bold("from Finder / File Explorer into this terminal window") + ". The full");
407
+ print(" path appears automatically. Press Enter.");
408
+ print("");
409
+ print(" " + dim("If drag-and-drop does not work:"));
410
+ print(" " + dim("Mac: right-click the folder, hold Option, choose"));
411
+ print(" " + dim(' "Copy as Pathname", then paste here with Cmd+V'));
412
+ print(" " + dim("Windows: Shift + right-click the folder, choose"));
413
+ print(" " + dim(' "Copy as path", then paste here with Ctrl+V'));
414
+ print("");
415
+ }
416
+
417
+ function saveLicenseFile(license) {
418
+ const dir = join(homedir(), ".shopos");
419
+ mkdirSync(dir, { recursive: true });
420
+ const path = join(dir, "license.json");
421
+ const record = {
422
+ key: license.key || null,
423
+ customer: license.customer,
424
+ product: license.product,
425
+ entitlements: license.entitlements,
426
+ valid_until: license.valid_until,
427
+ activated_at: new Date().toISOString(),
428
+ server: LICENSE_SERVER,
429
+ };
430
+ writeFileSync(path, JSON.stringify(record, null, 2) + "\n", "utf8");
431
+ try {
432
+ chmodSync(path, 0o600);
433
+ } catch {
434
+ // Windows: chmod is a no-op, ignore
435
+ }
436
+ return path;
437
+ }
438
+
439
+ // ---------- arg parsing ----------
440
+
441
+ function parseArgs(argv) {
442
+ const args = { license: null, vault: null, yes: false, existing: false, help: false };
443
+ for (let i = 0; i < argv.length; i++) {
444
+ const a = argv[i];
445
+ if (a === "--help" || a === "-h") args.help = true;
446
+ else if (a === "--yes" || a === "-y") args.yes = true;
447
+ else if (a === "--existing" || a === "-e") args.existing = true;
448
+ else if (a === "--license") args.license = argv[++i];
449
+ else if (a.startsWith("--license=")) args.license = a.slice("--license=".length);
450
+ else if (a === "--vault") args.vault = argv[++i];
451
+ else if (a.startsWith("--vault=")) args.vault = a.slice("--vault=".length);
452
+ }
453
+ return args;
454
+ }
455
+
456
+ function printHelp() {
457
+ print("");
458
+ print(bold("Usage:") + " npx @blueprintit/shop-os-install [options]");
459
+ print("");
460
+ print(bold("Options:"));
461
+ print(` --license <KEY> License key (skips interactive prompt)`);
462
+ print(` --vault <PATH> Vault location (skips interactive prompt)`);
463
+ print(` --existing, -e Add Shop OS to an existing vault (skips vault creation)`);
464
+ print(` --yes, -y Skip the install-here confirmation`);
465
+ print(` --help, -h Show this message`);
466
+ print("");
467
+ }
468
+
469
+ // ---------- main flow ----------
470
+
471
+ async function main() {
472
+ const args = parseArgs(process.argv.slice(2));
473
+ if (args.help) {
474
+ printHelp();
475
+ exit(0);
476
+ }
477
+
478
+ banner();
479
+
480
+ print(bold("Pre-flight checks"));
481
+ const nodeVersion = checkNode();
482
+ ok(`Node ${nodeVersion}`);
483
+ const claudeRoot = checkClaudeCode();
484
+ ok(`Claude Code detected at ${claudeRoot}`);
485
+ print("");
486
+
487
+ const rl = createInterface({ input: stdin, output: stdout });
488
+
489
+ let license;
490
+ // License entry: 1 attempt if --license flag given, else up to 3 interactive attempts.
491
+ const maxAttempts = args.license ? 1 : 3;
492
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
493
+ const key = args.license || (await ask(rl, "License key"));
494
+ if (!key) fail("No license key provided.");
495
+ print(" " + dim("Validating against license server..."));
496
+ const result = await validateLicense(key);
497
+ if (result.ok) {
498
+ license = { ...result.license, key };
499
+ ok(`License valid for ${bold(license.customer)}`);
500
+ info(`Product: ${license.product}`);
501
+ info(`Entitlements: ${license.entitlements.join(", ")}`);
502
+ break;
503
+ }
504
+ warn(`License rejected: ${result.error}`);
505
+ if (attempt < maxAttempts) {
506
+ print(" " + dim("Try again, or press Ctrl-C to cancel."));
507
+ } else {
508
+ print("");
509
+ fail("License validation failed. Reply to your welcome email for help: " + SUPPORT_URL);
510
+ }
511
+ }
512
+ print("");
513
+
514
+ // Vault mode: new or existing?
515
+ let isExisting = args.existing;
516
+ if (!isExisting && !args.vault) {
517
+ print(bold("Vault mode"));
518
+ print("");
519
+ print(" " + bold("new") + " Create a fresh Shop OS vault in a new folder");
520
+ print(" " + bold("existing") + " Add Shop OS to a vault you already have");
521
+ print("");
522
+ const modeAns = await ask(rl, "New vault or add to existing?", { default: "new" });
523
+ isExisting = modeAns.toLowerCase().startsWith("e");
524
+ print("");
525
+ }
526
+
527
+ // Vault location (flag overrides prompt)
528
+ let vaultPath = args.vault;
529
+ if (!vaultPath) {
530
+ if (isExisting) {
531
+ for (let attempt = 1; attempt <= 3; attempt++) {
532
+ const ans = await ask(rl, "Drag your existing vault folder here, then press Enter");
533
+ if (ans) { vaultPath = ans; break; }
534
+ warn("No path entered. Drag the folder into this window, or paste the copied path.");
535
+ }
536
+ } else {
537
+ printVaultLocationGuide();
538
+ for (let attempt = 1; attempt <= 3; attempt++) {
539
+ const ans = await ask(rl, "Drag your Shop OS Vault folder here, then press Enter");
540
+ if (ans) { vaultPath = ans; break; }
541
+ warn("No path entered. Drag the folder from Finder / File Explorer into this window, or paste the copied path.");
542
+ }
543
+ }
544
+ if (!vaultPath) {
545
+ rl.close();
546
+ fail("No vault path provided. Create the folder first, then re-run this installer.");
547
+ }
548
+ }
549
+ vaultPath = resolve(expandTilde(unwrapShellPath(vaultPath)));
550
+
551
+ if (!existsSync(vaultPath)) {
552
+ if (isExisting) {
553
+ rl.close();
554
+ fail(`No folder found at: ${vaultPath}\nMake sure the path is correct and the folder exists.`);
555
+ }
556
+ warn(`No folder found at: ${vaultPath}`);
557
+ const createIt = args.yes
558
+ ? true
559
+ : await confirm(rl, "Create it now and continue?", { default: false });
560
+ if (!createIt) {
561
+ rl.close();
562
+ fail("Create the folder in Finder / File Explorer first, then re-run this installer.");
563
+ }
564
+ }
565
+
566
+ const confirmMsg = isExisting
567
+ ? `Add Shop OS to existing vault at ${cyan(vaultPath)}?`
568
+ : `Install Shop OS into ${cyan(vaultPath)}?`;
569
+ const proceed = args.yes
570
+ ? true
571
+ : await confirm(rl, confirmMsg, { default: true });
572
+
573
+ if (!proceed) {
574
+ rl.close();
575
+ print("");
576
+ print(yellow("Cancelled. No changes made."));
577
+ exit(0);
578
+ }
579
+ rl.close();
580
+ print("");
581
+
582
+ // Step-by-step install
583
+ print(bold("Installing Shop OS"));
584
+
585
+ print(dim(" [1/6] Registering plugin marketplaces"));
586
+ const mpResult = ensureMarketplaces(claudeRoot);
587
+ if (mpResult.added.length === 0) {
588
+ info(`All ${mpResult.total} marketplaces already registered`);
589
+ } else {
590
+ for (const name of mpResult.added) ok(`Added marketplace: ${name}`);
591
+ }
592
+
593
+ print(dim(" [2/6] Enabling plugins for installation"));
594
+ const pluginsChanged = ensurePluginsInstalled(claudeRoot);
595
+ if (pluginsChanged) {
596
+ for (const id of PLUGINS_TO_ENABLE) ok(`Queued plugin: ${id}`);
597
+ info("Claude Code will sync the actual plugin files from the marketplaces on next launch.");
598
+ } else {
599
+ info("All required plugins already queued");
600
+ }
601
+
602
+ print(dim(` [3/6] ${isExisting ? "Configuring" : "Creating"} vault at ${vaultPath}`));
603
+ if (!existsSync(vaultPath)) {
604
+ mkdirSync(vaultPath, { recursive: true });
605
+ ok("Vault directory created");
606
+ } else {
607
+ info(`Vault directory ${isExisting ? "found" : "already exists"}`);
608
+ }
609
+ if (!isExisting) {
610
+ const wroteClaudeMd = createVaultClaudeMd(vaultPath, license);
611
+ if (wroteClaudeMd) ok("CLAUDE.md scaffolded");
612
+ else info("CLAUDE.md already present (left untouched)");
613
+ }
614
+
615
+ const rawResult = createRawInbox(vaultPath);
616
+ if (rawResult.created) ok("Raw/ inbox + Raw/processed/ created (drop materials in Raw/ to seed the vault)");
617
+ else info("Raw/ inbox already present (left untouched)");
618
+
619
+ print(dim(" [4/6] Enabling plugins for this vault"));
620
+ const settingsPath = enableForVault(vaultPath);
621
+ ok(`Wrote ${settingsPath.replace(homedir(), "~")}`);
622
+
623
+ print(dim(" [5/6] Saving license"));
624
+ const licensePath = saveLicenseFile(license);
625
+ ok(`License saved to ${licensePath.replace(homedir(), "~")} (chmod 600)`);
626
+
627
+ print(dim(" [6/6] Installing Shop OS Chat launcher"));
628
+ const launcherPath = writeChatLauncher(vaultPath);
629
+ ok(`Wrote ${launcherPath.replace(homedir(), "~")}`);
630
+
631
+ print("");
632
+ print(green(bold("✓ Shop OS installation complete!")));
633
+ print("");
634
+ print(bold("Next steps:"));
635
+ print(` 1. Open the ${cyan("Claude Code")} app you installed (Applications / Start menu)`);
636
+ print(` 2. Pick this folder when it asks which to open:`);
637
+ print(` ${cyan(vaultPath)}`);
638
+ print(` 3. In the Claude prompt, run ${cyan("/obsidian:os-setup")} to personalize your vault`);
639
+ print(` 4. Walk through the onboarding interview`);
640
+ print("");
641
+ print(` 5. To let your team chat with the vault (read-only),`);
642
+ print(` double-click ${cyan("Shop OS Chat.command")} (Mac) or ${cyan("Shop OS Chat.bat")} (Windows)`);
643
+ print(` in your vault folder. First launch downloads the chat (~20 seconds).`);
644
+ print("");
645
+ print(dim(`Support: ${SUPPORT_URL}`));
646
+ print("");
647
+ }
648
+
649
+ main().catch((err) => {
650
+ print("");
651
+ fail(`Unexpected error: ${err.message}`);
652
+ });
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@blueprintit/shop-os-install",
3
+ "version": "0.3.0",
4
+ "description": "One-command installer for Shop OS — Blueprint IT's AI Operating System for small businesses.",
5
+ "type": "module",
6
+ "bin": {
7
+ "shop-os-install": "./bin/shop-os-install.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "README.md"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18.0.0"
15
+ },
16
+ "keywords": [
17
+ "shop-os",
18
+ "blueprint-it",
19
+ "claude-code",
20
+ "obsidian",
21
+ "ai-os",
22
+ "cabinet-shop"
23
+ ],
24
+ "author": "Blueprint IT <info@blueprintit.ai>",
25
+ "license": "UNLICENSED",
26
+ "homepage": "https://blueprintit.ai/shop-os",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/blueprintit-ai/shop-os-installer.git"
30
+ }
31
+ }