slidemind 0.2.2 → 0.2.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/slidemind.mjs CHANGED
@@ -2,6 +2,9 @@
2
2
  // slidemind — single CLI entry point for the slidemind npm package.
3
3
  //
4
4
  // Subcommands:
5
+ // slidemind setup <claim> one-command onboarding from the add-in's
6
+ // "Connect CLI" button: connect + global
7
+ // install + skills + ready (run via npx)
5
8
  // slidemind init [--for <target>] set up slidemind in a coding harness
6
9
  // (deploy the skill);
7
10
  // targets: claude | codex | opencode | all
@@ -52,8 +55,17 @@ function printHelp() {
52
55
  `slidemind ${pkg.version} — drive the slidemind PowerPoint add-in to build native, on-brand decks\n` +
53
56
  `\n` +
54
57
  `Usage:\n` +
58
+ ` slidemind setup <claim> One-command onboarding. Copy the command from PowerPoint →\n` +
59
+ ` slidemind → Settings → General → Connect CLI and paste it:\n` +
60
+ ` it connects this machine, installs slidemind globally, deploys\n` +
61
+ ` the skills, and runs 'ready'. Designed for 'npx slidemind@latest'.\n` +
62
+ ` --for <target> harness to deploy skills into (default: auto — every\n` +
63
+ ` coding agent found on this machine; none to skip)\n` +
64
+ ` --no-install skip the global npm install (already installed)\n` +
65
+ ` --no-ready skip the closing readiness check\n` +
66
+ ` --json machine-readable result\n` +
55
67
  ` slidemind ready [deck] One-command session setup — run this FIRST each session.\n` +
56
- ` Verifies pairing/connection/CLI version, resolves the open\n` +
68
+ ` Verifies authorization/connection/CLI version, resolves the open\n` +
57
69
  ` design, materializes its bundle, and (with a deck) binds it.\n` +
58
70
  ` Exit 0 = ready; non-zero prints one actionable blocker.\n` +
59
71
  ` --to <presId> bind to a specific open window\n` +
@@ -132,7 +144,7 @@ function printHelp() {
132
144
  ` --force re-download even if the cached bundle is current\n` +
133
145
  ` slidemind presentations List active PowerPoint panes registered with the add-in\n` +
134
146
  ` --all include retained suspended entries\n` +
135
- ` slidemind doctor Diagnose CLI/add-in pairing, backend, and pane readiness\n` +
147
+ ` slidemind doctor Diagnose CLI/add-in authorization, backend, and pane readiness\n` +
136
148
  ` --json print machine-readable status\n` +
137
149
  ` slidemind read [presId|file.pptx] Pull the content of an open presentation (requires a host),\n` +
138
150
  ` or of a local .pptx path (read host-free from disk)\n` +
@@ -144,13 +156,12 @@ function printHelp() {
144
156
  ` --at <index> insert after slide N (0-based); default = append\n` +
145
157
  ` --replace delete existing slides first (destructive)\n` +
146
158
  ` --slides <ids> comma-separated source slide ids to push\n` +
147
- ` slidemind pair <code> Pair with an add-in (copy the code from its "Connect CLI")\n` +
148
- ` --label <name> name this environment (default: derived from URL)\n` +
149
- ` Pair once per environment; commands fan out across all of them.\n` +
150
- ` slidemind whoami Show every paired environment (* = default)\n` +
159
+ ` slidemind whoami Show every connected environment (* = default)\n` +
151
160
  ` slidemind use <url|env> Set the default environment\n` +
152
161
  ` slidemind logout [url|env] Unpair one environment (--all to unpair every one)\n` +
153
162
  ` slidemind init --for <t> Set up slidemind in a harness (deploys the skill; clears the bundle cache)\n` +
163
+ ` <t> = claude | codex | opencode | all | auto (every harness found here)\n` +
164
+ ` 'setup' already does this — only needed after installing a new agent\n` +
154
165
  ` slidemind uninstall --for <t> Remove a previously-installed skill (clears the bundle cache)\n` +
155
166
  ` slidemind update [--for <t>] Fetch the latest release and redeploy\n` +
156
167
  ` --for <t> harness to redeploy into (default: all)\n` +
@@ -1806,7 +1817,7 @@ function connectionBlocker(pub) {
1806
1817
  code: "not_paired",
1807
1818
  actor: "user",
1808
1819
  message:
1809
- "Not paired. In the add-in open Settings → General → Connect CLI, press Pair, then run the shown `slidemind pair <code>`.",
1820
+ "Not connected. In PowerPoint open slidemind → Settings → General → Connect CLI, press the link button, and run the command it shows.",
1810
1821
  };
1811
1822
  }
1812
1823
  if ((pub.backends ?? []).every((b) => !b.ok)) {
@@ -1876,6 +1887,226 @@ function connectionNextAction(pub) {
1876
1887
  return connectionBlocker(pub)?.message ?? "Ready.";
1877
1888
  }
1878
1889
 
1890
+ // ---------- one-command setup (npx bootstrap) ----------
1891
+ //
1892
+ // `npx --yes slidemind@latest setup <claim>` is the whole onboarding: the user
1893
+ // clicks "Connect CLI" in the add-in, pastes ONE command, and ends up with a
1894
+ // connected, globally-installed CLI. npx only runs the package from its cache —
1895
+ // it never installs it — so this command performs the persistent
1896
+ // `npm install -g` itself.
1897
+ //
1898
+ // Order matters: redeem FIRST. The claim is short-lived and one-time, while a
1899
+ // global npm install can take a while or hit an EACCES prefix problem. Spending
1900
+ // the claim up front means a failed install leaves a connected CLI the user can
1901
+ // fix at their leisure, instead of forcing them back to PowerPoint for a fresh
1902
+ // command.
1903
+ async function cmdSetup(rest) {
1904
+ let claimArg = null;
1905
+ // `auto` is the whole point: setup finishes onboarding by itself, so it
1906
+ // deploys into the coding agents this machine actually has instead of asking
1907
+ // the user to run `init` afterwards.
1908
+ let target = "auto";
1909
+ let doInstall = true;
1910
+ let doReady = true;
1911
+ let asJson = false;
1912
+ for (let i = 0; i < rest.length; i++) {
1913
+ const a = rest[i];
1914
+ if (a === "--for" || a === "-f") {
1915
+ target = rest[++i];
1916
+ if (!target) fail("--for expects a target");
1917
+ if (![...HARNESS_TARGETS, "all", "auto", "none"].includes(target)) {
1918
+ fail(`unknown install target: ${target}`);
1919
+ }
1920
+ } else if (a === "--no-install") {
1921
+ doInstall = false;
1922
+ } else if (a === "--no-ready") {
1923
+ doReady = false;
1924
+ } else if (a === "--json") {
1925
+ asJson = true;
1926
+ } else if (a.startsWith("-")) {
1927
+ fail(`unknown flag: ${a}`);
1928
+ } else if (claimArg === null) {
1929
+ claimArg = a;
1930
+ } else {
1931
+ fail("setup takes exactly one setup claim");
1932
+ }
1933
+ }
1934
+
1935
+ if (!claimArg) {
1936
+ fail(
1937
+ "setup needs the connection command from the add-in.\n" +
1938
+ " Open PowerPoint → slidemind → Settings → General → Connect CLI, copy the command, and paste it here.\n" +
1939
+ " Already have the CLI? The panel's `CLI already installed?` toggle gives the short `slidemind setup …` form.",
1940
+ );
1941
+ }
1942
+
1943
+ const pkg = readPackageJson();
1944
+ const claims = await loadBridgeModule("setup-claim");
1945
+ // Decode before any UI so a mistyped/partial paste fails in one clean line.
1946
+ const { backendURL, secret } = claims.decodeSetupClaim(claimArg);
1947
+ const cfg = await loadBridgeModule("config");
1948
+ const envLabel = cfg.deriveEnvLabel(backendURL);
1949
+
1950
+ // The installers paint their own progress rail. In --json mode stdout is
1951
+ // reserved for the payload, so silence the whole UI rather than letting it
1952
+ // corrupt what the caller parses.
1953
+ const ui = await loadInstallerUi();
1954
+ if (asJson) ui.silence();
1955
+ ui.banner(pkg.version, "setup");
1956
+
1957
+ // --- 1. spend the claim -------------------------------------------------
1958
+ await ui.section("connect", envLabel);
1959
+ const sessionToken = await claims.redeemSetupClaim({
1960
+ backendURL,
1961
+ secret,
1962
+ cliVersion: pkg.version,
1963
+ });
1964
+ cfg.upsertSession({ backendURL, sessionToken, label: envLabel });
1965
+ ui.step({ label: "authorize", value: envLabel, note: "session saved" });
1966
+
1967
+ // --- 2. make this CLI permanent ----------------------------------------
1968
+ // Without this the whole thing evaporates with npx's cache: the user would be
1969
+ // connected but have no `slidemind` on PATH.
1970
+ let installed = false;
1971
+ let installError = null;
1972
+ const pluginInstall = isClaudePluginInstall();
1973
+ if (doInstall && pluginInstall) {
1974
+ ui.step({
1975
+ status: "warn",
1976
+ label: "install",
1977
+ value: "skipped",
1978
+ note: "this slidemind came from the Claude Code plugin — update it with /plugin",
1979
+ });
1980
+ } else if (doInstall) {
1981
+ await ui.section("install", "npm global");
1982
+ ui.step({ label: "install", value: "slidemind@latest", note: "global (npm)" });
1983
+ try {
1984
+ await spawnInherit(npmBin(), [
1985
+ "install",
1986
+ "-g",
1987
+ "--loglevel=error",
1988
+ "slidemind@latest",
1989
+ ]);
1990
+ installed = true;
1991
+ } catch (err) {
1992
+ installError = err?.message || String(err);
1993
+ ui.step({
1994
+ status: "warn",
1995
+ label: "install",
1996
+ value: "failed",
1997
+ note: "the connection is saved — see the note below",
1998
+ });
1999
+ }
2000
+ }
2001
+
2002
+ // --- 3. deploy the skills ----------------------------------------------
2003
+ // In-process, from THIS package copy: the npx copy and the global install are
2004
+ // the same published version, and going through PATH here would fail on a
2005
+ // shell that has not rehashed yet.
2006
+ //
2007
+ // With the default `auto` this is what removes `slidemind init` from
2008
+ // onboarding: the skills land in every coding agent present on the machine,
2009
+ // and skip any whose plugin already supplies them (copying over a plugin's
2010
+ // skills plants a second set that drifts on the next plugin update).
2011
+ let skills = [];
2012
+ let skippedPlugins = [];
2013
+ if (target !== "none") {
2014
+ if (target === "auto") {
2015
+ const harnesses = await detectHarnesses();
2016
+ skippedPlugins = harnesses
2017
+ .filter((h) => h.present && h.pluginInstalled)
2018
+ .map((h) => h.target);
2019
+ }
2020
+ await ui.section(
2021
+ "skills",
2022
+ target === "auto"
2023
+ ? "detected harnesses"
2024
+ : target === "all"
2025
+ ? "all harnesses"
2026
+ : target,
2027
+ );
2028
+ skills = await runInstaller(target, "install", []);
2029
+ for (const t of skippedPlugins) {
2030
+ ui.step({
2031
+ status: "warn",
2032
+ label: t,
2033
+ value: "skipped",
2034
+ note: "its slidemind plugin already supplies the skills",
2035
+ });
2036
+ }
2037
+ if (skills.length === 0 && skippedPlugins.length === 0) {
2038
+ ui.step({
2039
+ status: "warn",
2040
+ label: "harnesses",
2041
+ value: "none detected",
2042
+ note: "install a coding agent, then run `slidemind init --for auto`",
2043
+ });
2044
+ }
2045
+ const bundleMod = await loadBridgeModule("bundle");
2046
+ bundleMod.clearBundleCache();
2047
+ }
2048
+
2049
+ if (asJson) {
2050
+ const globalInstall =
2051
+ doInstall && !pluginInstall
2052
+ ? installed
2053
+ ? "installed"
2054
+ : "failed"
2055
+ : "skipped";
2056
+ process.stdout.write(
2057
+ `${JSON.stringify(
2058
+ {
2059
+ ok: true,
2060
+ connected: true,
2061
+ env: envLabel,
2062
+ globalInstall,
2063
+ skills,
2064
+ // Present-but-plugin-served harnesses, so an agent can tell "nothing
2065
+ // deployed because nothing is installed" from "nothing needed".
2066
+ skippedPlugins,
2067
+ nextAction: "slidemind ready",
2068
+ // Only on the unhappy path: the session IS saved, so `ok` stays true,
2069
+ // but an agent needs to know the binary did not land.
2070
+ ...(installError ? { installError } : {}),
2071
+ },
2072
+ null,
2073
+ 2,
2074
+ )}\n`,
2075
+ );
2076
+ return;
2077
+ }
2078
+
2079
+ ui.done(
2080
+ target === "none" || (skills.length === 0 && skippedPlugins.length === 0)
2081
+ ? `connected to ${envLabel} — run 'slidemind ready' to start`
2082
+ : `connected to ${envLabel} and ready to use`,
2083
+ );
2084
+
2085
+ if (installError) {
2086
+ process.stderr.write(
2087
+ "\nslidemind is connected, but the global install could not write to npm's global package directory.\n" +
2088
+ "This usually happens on macOS/Linux when Node was installed with a system-owned npm prefix.\n\n" +
2089
+ "Avoid `sudo npm`. Use Homebrew/nvm, or move npm's global prefix into your home directory:\n\n" +
2090
+ " mkdir -p ~/.npm-global\n" +
2091
+ " npm config set prefix ~/.npm-global\n" +
2092
+ " echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.zshrc\n" +
2093
+ " source ~/.zshrc\n" +
2094
+ " npm install -g slidemind\n\n" +
2095
+ `(details: ${installError})\n`,
2096
+ );
2097
+ return;
2098
+ }
2099
+
2100
+ // --- 4. finish the session the user actually came for -------------------
2101
+ // `ready` exits non-zero for every legitimate not-yet blocker (PowerPoint
2102
+ // closed, no design activated). Setup already succeeded at that point, so its
2103
+ // blocker is information, not a failure — run it for the message and the
2104
+ // warmed bundle, and never inherit its exit code.
2105
+ if (doReady) {
2106
+ await spawnStatus(process.execPath, [__filename, "ready"]);
2107
+ }
2108
+ }
2109
+
1879
2110
  // The one-command agent preflight. Collapses the old 4-call session start
1880
2111
  // (presentations → pick → download-bundle → download-bundle-to-bind) into a
1881
2112
  // single verb: check pairing/reachability/CLI-version/windows, resolve the OPEN
@@ -1954,10 +2185,20 @@ async function cmdReady(rest) {
1954
2185
  fail(blocker.message);
1955
2186
  };
1956
2187
 
1957
- // --- Local: not paired short-circuits before any network. ---
2188
+ // --- First use: there is nothing this process can do on its own. Connecting
2189
+ // starts in the add-in because the claim it mints carries the deployment's
2190
+ // address, and an unconnected CLI has no way to learn which backend its user
2191
+ // belongs to. So name the one action that works and stop. ---
1958
2192
  const cfg = await loadBridgeModule("config");
1959
2193
  if (cfg.readSessions().length === 0) {
1960
- finishBlocker({ blocker: connectionBlocker({ paired: false }) });
2194
+ finishBlocker({
2195
+ blocker: {
2196
+ code: "not_connected",
2197
+ actor: "user",
2198
+ message:
2199
+ "Connect this CLI first: in PowerPoint open slidemind → Settings → General → Connect CLI, press the link button, copy the command it shows, and run it here.",
2200
+ },
2201
+ });
1961
2202
  return;
1962
2203
  }
1963
2204
 
@@ -2456,93 +2697,23 @@ async function loadBridgeModule(name) {
2456
2697
  return import(pathToFileURL(path.join(SRC_DIR, "bridge", `${name}.mjs`)).href);
2457
2698
  }
2458
2699
 
2459
- async function cmdPair(rest) {
2460
- let code = null;
2461
- let label = null;
2462
- for (let i = 0; i < rest.length; i++) {
2463
- const a = rest[i];
2464
- if (a === "--label") {
2465
- label = rest[++i];
2466
- if (!label) fail("--label expects a name");
2467
- } else if (!a.startsWith("--") && !code) {
2468
- code = a;
2469
- }
2470
- }
2471
- if (!code) {
2472
- fail('usage: slidemind pair <code> (copy the command from the add-in\'s "Connect CLI")');
2473
- }
2474
- const pairMod = await loadBridgeModule("pair");
2475
- const decoded = pairMod.decodePairingCode(code);
2476
- if (!decoded) {
2477
- fail("invalid pairing code — copy the exact `slidemind pair …` command shown in the add-in");
2478
- }
2479
- const res = await pairMod.httpJson(
2480
- "POST",
2481
- `${decoded.backendURL}/api/cli/pair/redeem`,
2482
- { body: { secret: decoded.secret } },
2483
- );
2484
- if (res.status !== 200 || !res.json?.sessionToken) {
2485
- fail(
2486
- `pairing failed (${res.status}): ${res.json?.error || "no session returned"}\n` +
2487
- "the code may have expired — generate a fresh one in the add-in's \"Connect CLI\"",
2488
- );
2489
- }
2490
- const cfg = await loadBridgeModule("config");
2491
- const sessions = cfg.upsertSession({
2492
- backendURL: decoded.backendURL,
2493
- sessionToken: res.json.sessionToken,
2494
- label,
2495
- });
2496
- const env = cfg.deriveEnvLabel(decoded.backendURL);
2497
- const others = sessions.length - 1;
2498
- const suffix =
2499
- others > 0
2500
- ? ` (${sessions.length} environments connected — commands now fan out across all of them)`
2501
- : "";
2502
- let verify = "";
2503
- try {
2504
- const client = await import(
2505
- pathToFileURL(path.join(SRC_DIR, "bridge", "client.mjs")).href
2506
- );
2507
- const deadline = Date.now() + 8000;
2508
- for (;;) {
2509
- const status = await client.connectionStatus();
2510
- if ((status.presentations ?? []).length > 0) {
2511
- verify = " Pane is registered.";
2512
- break;
2513
- }
2514
- if (Date.now() >= deadline) {
2515
- verify =
2516
- " No pane is registered yet; if PowerPoint is already open, reopen or click the slidemind task pane, then run 'slidemind doctor'.";
2517
- break;
2518
- }
2519
- await new Promise((resolve) => setTimeout(resolve, 1000));
2520
- }
2521
- } catch {
2522
- // Pairing itself succeeded; diagnostics remain available via doctor.
2523
- }
2524
- process.stdout.write(
2525
- `Paired with ${label || env}${suffix}.${verify} Run 'slidemind doctor' to verify.\n`,
2526
- );
2527
- }
2528
-
2529
2700
  async function cmdWhoami() {
2530
2701
  const cfg = await loadBridgeModule("config");
2531
2702
  const sessions = cfg.readSessions();
2532
2703
  if (sessions.length === 0) {
2533
2704
  process.stderr.write(
2534
- 'Not paired. Run `slidemind pair <code>` using the add-in\'s "Connect CLI".\n',
2705
+ "Not connected. In PowerPoint open slidemind → Settings → General → Connect CLI, press the link button, and run the command it shows.\n",
2535
2706
  );
2536
2707
  return;
2537
2708
  }
2538
- const pairMod = await loadBridgeModule("pair");
2709
+ const { httpJson } = await loadBridgeModule("http-json");
2539
2710
  const def = cfg.getDefaultBackend();
2540
2711
  const lines = await Promise.all(
2541
2712
  sessions.map(async (s) => {
2542
2713
  const marker = s.backendURL === def ? "*" : " ";
2543
2714
  let detail;
2544
2715
  try {
2545
- const res = await pairMod.httpJson(
2716
+ const res = await httpJson(
2546
2717
  "GET",
2547
2718
  `${s.backendURL}/api/cli/whoami`,
2548
2719
  { token: s.sessionToken },
@@ -2550,15 +2721,15 @@ async function cmdWhoami() {
2550
2721
  detail =
2551
2722
  res.status === 200 && res.json
2552
2723
  ? `user ${res.json.oid} · tenant ${res.json.tid ?? "(n/a)"}`
2553
- : `session invalid (${res.status}) — re-pair`;
2724
+ : `session invalid (${res.status}) — authorize again`;
2554
2725
  } catch {
2555
- detail = "unreachable — check your connection or re-pair";
2726
+ detail = "unreachable — check your connection or authorize again";
2556
2727
  }
2557
2728
  return `${marker} ${s.label}\n ${detail}`;
2558
2729
  }),
2559
2730
  );
2560
2731
  process.stdout.write(
2561
- `Paired environments (${sessions.length}):\n${lines.join("\n")}\n`,
2732
+ `Connected environments (${sessions.length}):\n${lines.join("\n")}\n`,
2562
2733
  );
2563
2734
  }
2564
2735
 
@@ -2593,10 +2764,10 @@ async function cmdLogout(rest) {
2593
2764
  toRemove = sessions.filter((s) => s.backendURL === def);
2594
2765
  }
2595
2766
 
2596
- const pairMod = await loadBridgeModule("pair");
2767
+ const { httpJson } = await loadBridgeModule("http-json");
2597
2768
  for (const s of toRemove) {
2598
2769
  try {
2599
- await pairMod.httpJson("POST", `${s.backendURL}/api/cli/logout`, {
2770
+ await httpJson("POST", `${s.backendURL}/api/cli/logout`, {
2600
2771
  token: s.sessionToken,
2601
2772
  });
2602
2773
  } catch {
@@ -2607,7 +2778,7 @@ async function cmdLogout(rest) {
2607
2778
  const remaining = cfg.readSessions().length;
2608
2779
  process.stdout.write(
2609
2780
  `Logged out of ${toRemove.map((s) => s.label).join(", ")}.` +
2610
- (remaining > 0 ? ` ${remaining} environment(s) still paired.\n` : "\n"),
2781
+ (remaining > 0 ? ` ${remaining} environment(s) still connected.\n` : "\n"),
2611
2782
  );
2612
2783
  }
2613
2784
 
@@ -2785,20 +2956,56 @@ async function cmdNew(rest) {
2785
2956
  );
2786
2957
  }
2787
2958
 
2788
- function parseTargetFlag(rest) {
2959
+ const HARNESS_TARGETS = ["claude", "codex", "opencode"];
2960
+
2961
+ function parseTargetFlag(rest, { allowAuto = false } = {}) {
2789
2962
  const idx = rest.findIndex((a) => a === "--for" || a === "-f");
2790
2963
  if (idx < 0 || !rest[idx + 1]) {
2791
- fail("missing --for <claude|codex|opencode|all>");
2964
+ fail(
2965
+ `missing --for <claude|codex|opencode|all${allowAuto ? "|auto" : ""}>`,
2966
+ );
2792
2967
  }
2793
2968
  const target = rest[idx + 1];
2794
- if (!["claude", "codex", "opencode", "all"].includes(target)) {
2969
+ const allowed = allowAuto
2970
+ ? [...HARNESS_TARGETS, "all", "auto"]
2971
+ : [...HARNESS_TARGETS, "all"];
2972
+ if (!allowed.includes(target)) {
2795
2973
  fail(`unknown install target: ${target}`);
2796
2974
  }
2797
2975
  return target;
2798
2976
  }
2799
2977
 
2978
+ // Which coding agents are actually on this machine, and which of them already
2979
+ // get their skills from a plugin. This is what lets `slidemind setup` finish the
2980
+ // job on its own: it deploys where a harness exists and stays out of the way
2981
+ // where a plugin already supplies the skills, so a separate `init` is no longer
2982
+ // part of onboarding.
2983
+ async function detectHarnesses() {
2984
+ const found = [];
2985
+ for (const target of HARNESS_TARGETS) {
2986
+ const mod = await import(
2987
+ pathToFileURL(path.join(INSTALLERS_DIR, `${target}.mjs`)).href
2988
+ );
2989
+ const info =
2990
+ typeof mod.detect === "function"
2991
+ ? mod.detect()
2992
+ : { present: false, pluginInstalled: false, root: null };
2993
+ found.push({ target, ...info });
2994
+ }
2995
+ return found;
2996
+ }
2997
+
2998
+ /** Expand a `--for` value into the concrete harness list to act on. */
2999
+ async function resolveInstallTargets(target) {
3000
+ if (target === "all") return [...HARNESS_TARGETS];
3001
+ if (target !== "auto") return [target];
3002
+ return (await detectHarnesses())
3003
+ .filter((h) => h.present && !h.pluginInstalled)
3004
+ .map((h) => h.target);
3005
+ }
3006
+
2800
3007
  async function runInstaller(target, action, rest) {
2801
- const targets = target === "all" ? ["claude", "codex", "opencode"] : [target];
3008
+ const targets = await resolveInstallTargets(target);
2802
3009
  for (const t of targets) {
2803
3010
  const mod = await import(
2804
3011
  pathToFileURL(path.join(INSTALLERS_DIR, `${t}.mjs`)).href
@@ -2812,6 +3019,7 @@ async function runInstaller(target, action, rest) {
2812
3019
  flags: rest,
2813
3020
  });
2814
3021
  }
3022
+ return targets;
2815
3023
  }
2816
3024
 
2817
3025
  async function loadInstallerUi() {
@@ -2822,10 +3030,18 @@ async function loadInstallerUi() {
2822
3030
  }
2823
3031
 
2824
3032
  async function cmdInstall(rest) {
2825
- const target = parseTargetFlag(rest);
3033
+ const target = parseTargetFlag(rest, { allowAuto: true });
2826
3034
  const ui = await loadInstallerUi();
2827
3035
  ui.banner(readPackageJson().version, "init");
2828
- await runInstaller(target, "install", rest);
3036
+ const deployed = await runInstaller(target, "install", rest);
3037
+ if (deployed.length === 0) {
3038
+ ui.step({
3039
+ status: "warn",
3040
+ label: "harnesses",
3041
+ value: "none detected",
3042
+ note: "install a coding agent, then rerun with --for <claude|codex|opencode>",
3043
+ });
3044
+ }
2829
3045
 
2830
3046
  // Force every design bundle to re-download from the add-in on the next
2831
3047
  // build/push/download by wiping the materialized cache. init is the
@@ -2843,9 +3059,11 @@ async function cmdInstall(rest) {
2843
3059
  });
2844
3060
 
2845
3061
  ui.done(
2846
- target === "all"
2847
- ? "ready — ask your agent to plan, create, or edit a deck"
2848
- : `ready — ${target} can now plan, create, or edit decks`,
3062
+ deployed.length === 0
3063
+ ? "no coding agent found — the CLI is installed, skills are not"
3064
+ : deployed.length === HARNESS_TARGETS.length
3065
+ ? "ready — ask your agent to plan, create, or edit a deck"
3066
+ : `ready — ${deployed.join(", ")} can now plan, create, or edit decks`,
2849
3067
  );
2850
3068
  }
2851
3069
 
@@ -2900,6 +3118,16 @@ function spawnInherit(file, args) {
2900
3118
  });
2901
3119
  }
2902
3120
 
3121
+ // Like spawnInherit, but resolves with the child's exit code instead of
3122
+ // rejecting — for a step whose failure is a message to the user rather than a
3123
+ // failure of the command that ran it (see `setup` → `ready`).
3124
+ function spawnStatus(file, args) {
3125
+ return spawnInherit(file, args).then(
3126
+ () => 0,
3127
+ () => 1,
3128
+ );
3129
+ }
3130
+
2903
3131
  // ---------- distribution (public npm) ----------
2904
3132
  //
2905
3133
  // slidemind is published to public npm as `slidemind`. First install is
@@ -3070,12 +3298,12 @@ async function main(rawArgv) {
3070
3298
  case "ready":
3071
3299
  await cmdReady(rest);
3072
3300
  return 0;
3301
+ case "setup":
3302
+ await cmdSetup(rest);
3303
+ return 0;
3073
3304
  case "new":
3074
3305
  await cmdNew(rest);
3075
3306
  return 0;
3076
- case "pair":
3077
- await cmdPair(rest);
3078
- break;
3079
3307
  case "whoami":
3080
3308
  await cmdWhoami(rest);
3081
3309
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slidemind",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Connect Claude Code, Codex & opencode to the slidemind PowerPoint add-in — let your coding agent build and edit native, on-brand PowerPoint decks against your active corporate design, from the terminal.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,6 +21,7 @@
21
21
  ".claude-plugin/",
22
22
  ".codex-plugin/",
23
23
  ".agents/plugins/",
24
+ "assets/",
24
25
  "AGENTS.md",
25
26
  "README.md"
26
27
  ],