slidemind 0.3.13 → 0.3.14
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +86 -21
- package/README.md +14 -17
- package/bin/slidemind.mjs +65 -4969
- package/package.json +1 -1
- package/skills/create-slides/SKILL.md +113 -74
- package/skills/search/SKILL.md +25 -7
- package/src/bridge/build-and-push.mjs +108 -15
- package/src/bridge/bundle.mjs +69 -0
- package/src/bridge/capture.mjs +19 -0
- package/src/bridge/client/brands.mjs +622 -0
- package/src/bridge/client/engine-routing.mjs +237 -0
- package/src/bridge/client/engine-search.mjs +310 -0
- package/src/bridge/client/engine-slides.mjs +435 -0
- package/src/bridge/client/pane-ops.mjs +335 -0
- package/src/bridge/client/presentations.mjs +587 -0
- package/src/bridge/client/render.mjs +317 -0
- package/src/bridge/client/transport.mjs +695 -0
- package/src/bridge/client.mjs +80 -3452
- package/src/bridge/config.mjs +1 -1
- package/src/bridge/engine-download.mjs +15 -2
- package/src/builders/build.mjs +11 -804
- package/src/builders/draft-check.mjs +376 -0
- package/src/builders/lint.mjs +38 -2
- package/src/builders/preview.html +799 -0
- package/src/builders/review.mjs +23 -4
- package/src/builders/visual-families.mjs +6 -8
- package/src/cli/common.mjs +179 -0
- package/src/cli/concepts.mjs +310 -0
- package/src/cli/connection.mjs +147 -0
- package/src/cli/deck.mjs +608 -0
- package/src/cli/designs.mjs +113 -0
- package/src/cli/diagnose.mjs +141 -0
- package/src/cli/download-bundle.mjs +144 -0
- package/src/cli/engine-helpers.mjs +429 -0
- package/src/cli/engine-results.mjs +320 -0
- package/src/cli/help.mjs +201 -0
- package/src/cli/install.mjs +257 -0
- package/src/cli/read.mjs +120 -0
- package/src/cli/ready.mjs +805 -0
- package/src/cli/search.mjs +608 -0
- package/src/cli/session.mjs +177 -0
- package/src/cli/setup.mjs +282 -0
- package/src/cli/slide-reuse.mjs +334 -0
- package/src/cli/status.mjs +118 -0
- package/src/cli/workspace.mjs +302 -0
- package/src/installers/codex.mjs +1 -1
- package/src/update-check.mjs +231 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { envPhrase } from "../bridge/config.mjs";
|
|
2
|
+
import { fail, loadBridgeModule } from "./common.mjs";
|
|
3
|
+
|
|
4
|
+
// The one line every "you are not connected" path speaks, so the remedy reads
|
|
5
|
+
// the same wherever a user meets it.
|
|
6
|
+
export const NOT_CONNECTED_LINE =
|
|
7
|
+
"Not connected. In PowerPoint open slidemind → Settings → General → Connect CLI, press the link button, and run the command it shows.";
|
|
8
|
+
|
|
9
|
+
// Match a `--backend`-style argument against a paired session the way logout and
|
|
10
|
+
// use do: exact URL, stored label, derived env label, or a substring of the URL.
|
|
11
|
+
export function matchesBackend(cfg, session, want) {
|
|
12
|
+
return (
|
|
13
|
+
session.backendURL === want ||
|
|
14
|
+
session.label === want ||
|
|
15
|
+
cfg.deriveEnvLabel(session.backendURL) === want ||
|
|
16
|
+
session.backendURL.includes(want)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// `slidemind login [--backend <url|env>]` — sign this CLI in to Microsoft 365 so
|
|
21
|
+
// the pane-free engine routes can fetch SharePoint-backed content with the
|
|
22
|
+
// user's OWN identity (docs/slidemind-cli-connect/15-graph-delegated-cli.md).
|
|
23
|
+
// Separate from the CLI session on purpose: that one is minted by the add-in for
|
|
24
|
+
// whoever pressed Connect CLI, this one is an Entra token for whoever completes
|
|
25
|
+
// the browser sign-in, and only the second may read a user's SharePoint content.
|
|
26
|
+
// Everything else — render, bundle, whoami — is unaffected either way.
|
|
27
|
+
export async function cmdLogin(rest) {
|
|
28
|
+
const stray = rest.find((a) => a.startsWith("-"));
|
|
29
|
+
if (stray) {
|
|
30
|
+
fail(`unexpected argument: ${stray}\nusage: slidemind login [--backend <url|env>]`);
|
|
31
|
+
}
|
|
32
|
+
const cfg = await loadBridgeModule("config");
|
|
33
|
+
const sessions = cfg.readSessions();
|
|
34
|
+
if (sessions.length === 0) fail(NOT_CONNECTED_LINE);
|
|
35
|
+
// A browser flow targets ONE environment — there is nothing to fan out. The
|
|
36
|
+
// scope flag picks it; otherwise the default environment does, and the closing
|
|
37
|
+
// line names the others so nothing looks silently skipped.
|
|
38
|
+
const want = (process.env.SLIDEMIND_BACKEND || "").trim();
|
|
39
|
+
let target;
|
|
40
|
+
if (want) {
|
|
41
|
+
target = sessions.find((s) => matchesBackend(cfg, s, want));
|
|
42
|
+
if (!target) {
|
|
43
|
+
fail(`not paired with '${want}'. Run 'slidemind whoami' to list environments.`);
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
const def = cfg.getDefaultBackend();
|
|
47
|
+
target = sessions.find((s) => s.backendURL === def) ?? sessions[0];
|
|
48
|
+
}
|
|
49
|
+
const graph = await loadBridgeModule("graph-auth");
|
|
50
|
+
// This command is routinely typed by an AI agent on the human's machine, and
|
|
51
|
+
// the browser is where the agent's reach ends — say the handoff out loud.
|
|
52
|
+
process.stderr.write(
|
|
53
|
+
`Opening your browser to sign in to Microsoft 365${envPhrase(target.label, "for")}…\n` +
|
|
54
|
+
"A HUMAN must complete this step (agents: ask your user to finish the " +
|
|
55
|
+
"sign-in in the browser window that just opened).\n",
|
|
56
|
+
);
|
|
57
|
+
const res = await graph.loginGraph({
|
|
58
|
+
backendURL: target.backendURL,
|
|
59
|
+
// Printed before the opener runs: on a machine where no opener works this
|
|
60
|
+
// link is the only way through, and it is useless after the wait times out.
|
|
61
|
+
onUrl: (url) =>
|
|
62
|
+
process.stderr.write(`If no browser opens, paste this link into one:\n ${url}\n`),
|
|
63
|
+
});
|
|
64
|
+
process.stdout.write(
|
|
65
|
+
`Signed in to Microsoft 365 as ${res.username ?? "your account"}${envPhrase(target.label, "for")}.\n` +
|
|
66
|
+
"SharePoint-backed slides and images now download with no PowerPoint window open.\n",
|
|
67
|
+
);
|
|
68
|
+
const others = sessions.filter((s) => s.backendURL !== target.backendURL);
|
|
69
|
+
if (others.length > 0) {
|
|
70
|
+
process.stdout.write(
|
|
71
|
+
`Still signed out: ${others.map((s) => s.label).join(", ")} ` +
|
|
72
|
+
"(each needs its own 'slidemind login --backend <env>').\n",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function cmdWhoami() {
|
|
78
|
+
const cfg = await loadBridgeModule("config");
|
|
79
|
+
const sessions = cfg.readSessions();
|
|
80
|
+
if (sessions.length === 0) {
|
|
81
|
+
process.stderr.write(`${NOT_CONNECTED_LINE}\n`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const { httpJson } = await loadBridgeModule("http-json");
|
|
85
|
+
const graph = await loadBridgeModule("graph-auth");
|
|
86
|
+
const def = cfg.getDefaultBackend();
|
|
87
|
+
const lines = await Promise.all(
|
|
88
|
+
sessions.map(async (s) => {
|
|
89
|
+
const marker = s.backendURL === def ? "*" : " ";
|
|
90
|
+
let detail;
|
|
91
|
+
try {
|
|
92
|
+
const res = await httpJson(
|
|
93
|
+
"GET",
|
|
94
|
+
`${s.backendURL}/api/cli/whoami`,
|
|
95
|
+
{ token: s.sessionToken },
|
|
96
|
+
);
|
|
97
|
+
detail =
|
|
98
|
+
res.status === 200 && res.json
|
|
99
|
+
? `user ${res.json.oid} · tenant ${res.json.tid ?? "(n/a)"}`
|
|
100
|
+
: `session invalid (${res.status}) — authorize again`;
|
|
101
|
+
} catch {
|
|
102
|
+
detail = "unreachable — check your connection or authorize again";
|
|
103
|
+
}
|
|
104
|
+
// Storage-only — no probe. The Graph sign-in is a SECOND identity, so it
|
|
105
|
+
// is reported next to the session rather than folded into it.
|
|
106
|
+
const graphLine = graph.hasGraphSession(s.backendURL)
|
|
107
|
+
? "Microsoft 365: signed in"
|
|
108
|
+
: "Microsoft 365: not signed in — needed only for window-free SharePoint downloads (`slidemind login`)";
|
|
109
|
+
return `${marker} ${s.label}\n ${detail}\n ${graphLine}`;
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
process.stdout.write(
|
|
113
|
+
`Connected environments (${sessions.length}):\n${lines.join("\n")}\n`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function cmdLogout(rest) {
|
|
118
|
+
const cfg = await loadBridgeModule("config");
|
|
119
|
+
const all = rest.includes("--all");
|
|
120
|
+
const target = rest.find((a) => !a.startsWith("--")) ?? null;
|
|
121
|
+
|
|
122
|
+
// Which sessions to revoke + remove: all, one (by url/env/label), or the
|
|
123
|
+
// default (the bare `slidemind logout`).
|
|
124
|
+
const sessions = cfg.readSessions();
|
|
125
|
+
if (sessions.length === 0) {
|
|
126
|
+
process.stdout.write("Already logged out.\n");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let toRemove;
|
|
130
|
+
if (all) {
|
|
131
|
+
toRemove = sessions;
|
|
132
|
+
} else if (target) {
|
|
133
|
+
toRemove = sessions.filter(
|
|
134
|
+
(s) =>
|
|
135
|
+
s.backendURL === target ||
|
|
136
|
+
s.label === target ||
|
|
137
|
+
cfg.deriveEnvLabel(s.backendURL) === target ||
|
|
138
|
+
s.backendURL.includes(target),
|
|
139
|
+
);
|
|
140
|
+
if (toRemove.length === 0) {
|
|
141
|
+
fail(`not paired with '${target}'. Run 'slidemind whoami' to list environments.`);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
const def = cfg.getDefaultBackend();
|
|
145
|
+
toRemove = sessions.filter((s) => s.backendURL === def);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const { httpJson } = await loadBridgeModule("http-json");
|
|
149
|
+
// Unpairing takes the WHOLE record, the Microsoft 365 refresh token included —
|
|
150
|
+
// it was stored for this environment's session and must not outlive it. Said
|
|
151
|
+
// out loud because signing back in is a second command, not part of `setup`.
|
|
152
|
+
const droppedGraph = toRemove.filter((s) => s.msGraphRefreshToken);
|
|
153
|
+
for (const s of toRemove) {
|
|
154
|
+
try {
|
|
155
|
+
await httpJson("POST", `${s.backendURL}/api/cli/logout`, {
|
|
156
|
+
token: s.sessionToken,
|
|
157
|
+
});
|
|
158
|
+
} catch {
|
|
159
|
+
/* best-effort server-side revoke; we still drop the local session */
|
|
160
|
+
}
|
|
161
|
+
// Through the token setter, not just the record removal: the refresh token
|
|
162
|
+
// may live in the macOS keychain (config.mjs), which removing the session
|
|
163
|
+
// record alone would orphan.
|
|
164
|
+
cfg.setGraphRefreshToken(s.backendURL, null);
|
|
165
|
+
cfg.removeSession(s.backendURL);
|
|
166
|
+
}
|
|
167
|
+
const remaining = cfg.readSessions().length;
|
|
168
|
+
process.stdout.write(
|
|
169
|
+
`Logged out of ${toRemove.map((s) => s.label).join(", ")}.` +
|
|
170
|
+
(remaining > 0 ? ` ${remaining} environment(s) still connected.\n` : "\n") +
|
|
171
|
+
(droppedGraph.length > 0
|
|
172
|
+
? `The Microsoft 365 sign-in for ${droppedGraph
|
|
173
|
+
.map((s) => s.label)
|
|
174
|
+
.join(", ")} was dropped too — run 'slidemind login' after reconnecting.\n`
|
|
175
|
+
: ""),
|
|
176
|
+
);
|
|
177
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { isClaudePluginInstall } from "../install-channel.mjs";
|
|
2
|
+
import { npmBin } from "../update-check.mjs";
|
|
3
|
+
import { readPackageJson, fail, loadBridgeModule, BIN_PATH } from "./common.mjs";
|
|
4
|
+
import { HARNESS_TARGETS, detectHarnesses, runInstaller, loadInstallerUi, spawnInherit, spawnStatus } from "./install.mjs";
|
|
5
|
+
|
|
6
|
+
//
|
|
7
|
+
// `npx --yes slidemind@latest setup <claim>` is the whole onboarding: the user
|
|
8
|
+
// clicks "Connect CLI" in the add-in, pastes ONE command, and ends up with a
|
|
9
|
+
// connected, globally-installed CLI. npx only runs the package from its cache —
|
|
10
|
+
// it never installs it — so this command performs the persistent
|
|
11
|
+
// `npm install -g` itself.
|
|
12
|
+
//
|
|
13
|
+
// Order matters: redeem FIRST. The claim is short-lived and one-time, while a
|
|
14
|
+
// global npm install can take a while or hit an EACCES prefix problem. Spending
|
|
15
|
+
// the claim up front means a failed install leaves a connected CLI the user can
|
|
16
|
+
// fix at their leisure, instead of forcing them back to PowerPoint for a fresh
|
|
17
|
+
// command.
|
|
18
|
+
export async function cmdSetup(rest) {
|
|
19
|
+
let claimArg = null;
|
|
20
|
+
// `auto` is the whole point: setup finishes onboarding by itself, so it
|
|
21
|
+
// deploys into the coding agents this machine actually has instead of asking
|
|
22
|
+
// the user to run `init` afterwards.
|
|
23
|
+
let target = "auto";
|
|
24
|
+
let doInstall = true;
|
|
25
|
+
let doReady = true;
|
|
26
|
+
let doLogin = true;
|
|
27
|
+
let asJson = false;
|
|
28
|
+
for (let i = 0; i < rest.length; i++) {
|
|
29
|
+
const a = rest[i];
|
|
30
|
+
if (a === "--for" || a === "-f") {
|
|
31
|
+
target = rest[++i];
|
|
32
|
+
if (!target) fail("--for expects a target");
|
|
33
|
+
if (![...HARNESS_TARGETS, "all", "auto", "none"].includes(target)) {
|
|
34
|
+
fail(`unknown install target: ${target}`);
|
|
35
|
+
}
|
|
36
|
+
} else if (a === "--no-install") {
|
|
37
|
+
doInstall = false;
|
|
38
|
+
} else if (a === "--no-ready") {
|
|
39
|
+
doReady = false;
|
|
40
|
+
} else if (a === "--no-login") {
|
|
41
|
+
doLogin = false;
|
|
42
|
+
} else if (a === "--json") {
|
|
43
|
+
asJson = true;
|
|
44
|
+
} else if (a.startsWith("-")) {
|
|
45
|
+
fail(`unknown flag: ${a}`);
|
|
46
|
+
} else if (claimArg === null) {
|
|
47
|
+
claimArg = a;
|
|
48
|
+
} else {
|
|
49
|
+
fail("setup takes exactly one setup claim");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!claimArg) {
|
|
54
|
+
fail(
|
|
55
|
+
"setup needs the connection command from the add-in.\n" +
|
|
56
|
+
" Open PowerPoint → slidemind → Settings → General → Connect CLI, copy the command, and paste it here.\n" +
|
|
57
|
+
" Already have the CLI? The panel's `CLI already installed?` toggle gives the short `slidemind setup …` form.",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const pkg = readPackageJson();
|
|
62
|
+
const claims = await loadBridgeModule("setup-claim");
|
|
63
|
+
// Decode before any UI so a mistyped/partial paste fails in one clean line.
|
|
64
|
+
const { backendURL, secret } = claims.decodeSetupClaim(claimArg);
|
|
65
|
+
const cfg = await loadBridgeModule("config");
|
|
66
|
+
const envLabel = cfg.deriveEnvLabel(backendURL);
|
|
67
|
+
|
|
68
|
+
// The installers paint their own progress rail. In --json mode stdout is
|
|
69
|
+
// reserved for the payload, so silence the whole UI rather than letting it
|
|
70
|
+
// corrupt what the caller parses.
|
|
71
|
+
const ui = await loadInstallerUi();
|
|
72
|
+
if (asJson) ui.silence();
|
|
73
|
+
ui.banner(pkg.version, "setup");
|
|
74
|
+
|
|
75
|
+
// --- 1. spend the claim -------------------------------------------------
|
|
76
|
+
// Whether to name the environment at all. Decided BEFORE the claim is spent
|
|
77
|
+
// so every line in this run agrees: a first connection has nothing to
|
|
78
|
+
// disambiguate against and stays quiet, while adding a second (or third)
|
|
79
|
+
// environment names each one. Computing it per-line would flip mid-run, since
|
|
80
|
+
// `upsertSession` below changes the count.
|
|
81
|
+
const showEnv = cfg.readSessions().length > 0;
|
|
82
|
+
await ui.section("connect", showEnv ? envLabel : "");
|
|
83
|
+
const { sessionToken, oid } = await claims.redeemSetupClaim({
|
|
84
|
+
backendURL,
|
|
85
|
+
secret,
|
|
86
|
+
cliVersion: pkg.version,
|
|
87
|
+
});
|
|
88
|
+
cfg.upsertSession({ backendURL, sessionToken, label: envLabel, oid });
|
|
89
|
+
ui.step({
|
|
90
|
+
label: "authorize",
|
|
91
|
+
value: showEnv ? envLabel : "connected",
|
|
92
|
+
note: "session saved",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// --- 2. make this CLI permanent ----------------------------------------
|
|
96
|
+
// Without this the whole thing evaporates with npx's cache: the user would be
|
|
97
|
+
// connected but have no `slidemind` on PATH.
|
|
98
|
+
let installed = false;
|
|
99
|
+
let installError = null;
|
|
100
|
+
const pluginInstall = isClaudePluginInstall();
|
|
101
|
+
if (doInstall && pluginInstall) {
|
|
102
|
+
ui.step({
|
|
103
|
+
status: "warn",
|
|
104
|
+
label: "install",
|
|
105
|
+
value: "skipped",
|
|
106
|
+
note: "this slidemind came from the Claude Code plugin — update it with /plugin",
|
|
107
|
+
});
|
|
108
|
+
} else if (doInstall) {
|
|
109
|
+
await ui.section("install", "npm global");
|
|
110
|
+
ui.step({ label: "install", value: "slidemind@latest", note: "global (npm)" });
|
|
111
|
+
try {
|
|
112
|
+
await spawnInherit(npmBin(), [
|
|
113
|
+
"install",
|
|
114
|
+
"-g",
|
|
115
|
+
"--loglevel=error",
|
|
116
|
+
"slidemind@latest",
|
|
117
|
+
]);
|
|
118
|
+
installed = true;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
installError = err?.message || String(err);
|
|
121
|
+
ui.step({
|
|
122
|
+
status: "warn",
|
|
123
|
+
label: "install",
|
|
124
|
+
value: "failed",
|
|
125
|
+
note: "the connection is saved — see the note below",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- 3. deploy the skills ----------------------------------------------
|
|
131
|
+
// In-process, from THIS package copy: the npx copy and the global install are
|
|
132
|
+
// the same published version, and going through PATH here would fail on a
|
|
133
|
+
// shell that has not rehashed yet.
|
|
134
|
+
//
|
|
135
|
+
// With the default `auto` this is what removes `slidemind init` from
|
|
136
|
+
// onboarding: the skills land in every coding agent present on the machine,
|
|
137
|
+
// and skip any whose plugin already supplies them (copying over a plugin's
|
|
138
|
+
// skills plants a second set that drifts on the next plugin update).
|
|
139
|
+
let skills = [];
|
|
140
|
+
let skippedPlugins = [];
|
|
141
|
+
if (target !== "none") {
|
|
142
|
+
if (target === "auto") {
|
|
143
|
+
const harnesses = await detectHarnesses();
|
|
144
|
+
skippedPlugins = harnesses
|
|
145
|
+
.filter((h) => h.present && h.pluginInstalled)
|
|
146
|
+
.map((h) => h.target);
|
|
147
|
+
}
|
|
148
|
+
await ui.section(
|
|
149
|
+
"skills",
|
|
150
|
+
target === "auto"
|
|
151
|
+
? "detected harnesses"
|
|
152
|
+
: target === "all"
|
|
153
|
+
? "all harnesses"
|
|
154
|
+
: target,
|
|
155
|
+
);
|
|
156
|
+
skills = await runInstaller(target, "install", []);
|
|
157
|
+
for (const t of skippedPlugins) {
|
|
158
|
+
ui.step({
|
|
159
|
+
status: "warn",
|
|
160
|
+
label: t,
|
|
161
|
+
value: "skipped",
|
|
162
|
+
note: "its slidemind plugin already supplies the skills",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (skills.length === 0 && skippedPlugins.length === 0) {
|
|
166
|
+
ui.step({
|
|
167
|
+
status: "warn",
|
|
168
|
+
label: "harnesses",
|
|
169
|
+
value: "none detected",
|
|
170
|
+
note: "install a coding agent, then run `slidemind init --for auto`",
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
const bundleMod = await loadBridgeModule("bundle");
|
|
174
|
+
bundleMod.clearBundleCache();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// --- 4. Microsoft 365 sign-in (best-effort) -----------------------------
|
|
178
|
+
// The pasted-claim moment is the one time the user is reliably at the
|
|
179
|
+
// keyboard in consent headspace, so offer the browser sign-in NOW and no
|
|
180
|
+
// later command has to interrupt a task for it. Strictly best-effort and
|
|
181
|
+
// interactive-only: --json or no TTY (an agent ran setup — a browser tab it
|
|
182
|
+
// cannot see must never block onboarding), --no-login, a backend that does
|
|
183
|
+
// not honour the token, a decline or a timeout all skip WITHOUT failing
|
|
184
|
+
// setup — the closing `ready` and the failure-time remediations still name
|
|
185
|
+
// `slidemind login`.
|
|
186
|
+
if (doLogin && !asJson && process.stdout.isTTY) {
|
|
187
|
+
try {
|
|
188
|
+
const client = await loadBridgeModule("client");
|
|
189
|
+
if (await client.backendSupportsGraphDelegated(backendURL)) {
|
|
190
|
+
await ui.section("sign in", "Microsoft 365");
|
|
191
|
+
process.stderr.write(
|
|
192
|
+
"Opening your browser to sign in to Microsoft 365 — this lets SharePoint-backed\n" +
|
|
193
|
+
"content download with no PowerPoint window open.\n",
|
|
194
|
+
);
|
|
195
|
+
const graph = await loadBridgeModule("graph-auth");
|
|
196
|
+
const res = await graph.loginGraph({
|
|
197
|
+
backendURL,
|
|
198
|
+
// Shorter than `slidemind login`'s: the user is mid-setup, not
|
|
199
|
+
// parked on a sign-in they asked for — a walked-away browser tab
|
|
200
|
+
// should not stall onboarding for five minutes.
|
|
201
|
+
timeoutMs: 120_000,
|
|
202
|
+
onUrl: (url) =>
|
|
203
|
+
process.stderr.write(
|
|
204
|
+
`If no browser opens, paste this link into one:\n ${url}\n`,
|
|
205
|
+
),
|
|
206
|
+
});
|
|
207
|
+
ui.step({
|
|
208
|
+
label: "Microsoft 365",
|
|
209
|
+
value: res.username ?? "signed in",
|
|
210
|
+
...(showEnv ? { note: envLabel } : {}),
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
ui.step({
|
|
215
|
+
status: "warn",
|
|
216
|
+
label: "Microsoft 365",
|
|
217
|
+
value: "skipped",
|
|
218
|
+
note: "run 'slidemind login' anytime for window-free SharePoint downloads",
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (asJson) {
|
|
224
|
+
const globalInstall =
|
|
225
|
+
doInstall && !pluginInstall
|
|
226
|
+
? installed
|
|
227
|
+
? "installed"
|
|
228
|
+
: "failed"
|
|
229
|
+
: "skipped";
|
|
230
|
+
process.stdout.write(
|
|
231
|
+
`${JSON.stringify(
|
|
232
|
+
{
|
|
233
|
+
ok: true,
|
|
234
|
+
connected: true,
|
|
235
|
+
env: envLabel,
|
|
236
|
+
globalInstall,
|
|
237
|
+
skills,
|
|
238
|
+
// Present-but-plugin-served harnesses, so an agent can tell "nothing
|
|
239
|
+
// deployed because nothing is installed" from "nothing needed".
|
|
240
|
+
skippedPlugins,
|
|
241
|
+
nextAction: "slidemind ready",
|
|
242
|
+
// Only on the unhappy path: the session IS saved, so `ok` stays true,
|
|
243
|
+
// but an agent needs to know the binary did not land.
|
|
244
|
+
...(installError ? { installError } : {}),
|
|
245
|
+
},
|
|
246
|
+
null,
|
|
247
|
+
2,
|
|
248
|
+
)}\n`,
|
|
249
|
+
);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
ui.done(
|
|
254
|
+
target === "none" || (skills.length === 0 && skippedPlugins.length === 0)
|
|
255
|
+
? `connected${showEnv ? ` to ${envLabel}` : ""} — run 'slidemind ready' to start`
|
|
256
|
+
: `connected${showEnv ? ` to ${envLabel}` : ""} and ready to use`,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
if (installError) {
|
|
260
|
+
process.stderr.write(
|
|
261
|
+
"\nslidemind is connected, but the global install could not write to npm's global package directory.\n" +
|
|
262
|
+
"This usually happens on macOS/Linux when Node was installed with a system-owned npm prefix.\n\n" +
|
|
263
|
+
"Avoid `sudo npm`. Use Homebrew/nvm, or move npm's global prefix into your home directory:\n\n" +
|
|
264
|
+
" mkdir -p ~/.npm-global\n" +
|
|
265
|
+
" npm config set prefix ~/.npm-global\n" +
|
|
266
|
+
" echo 'export PATH=\"$HOME/.npm-global/bin:$PATH\"' >> ~/.zshrc\n" +
|
|
267
|
+
" source ~/.zshrc\n" +
|
|
268
|
+
" npm install -g slidemind\n\n" +
|
|
269
|
+
`(details: ${installError})\n`,
|
|
270
|
+
);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// --- 5. finish the session the user actually came for -------------------
|
|
275
|
+
// `ready` exits non-zero for every legitimate not-yet blocker (PowerPoint
|
|
276
|
+
// closed, no design activated). Setup already succeeded at that point, so its
|
|
277
|
+
// blocker is information, not a failure — run it for the message and the
|
|
278
|
+
// warmed bundle, and never inherit its exit code.
|
|
279
|
+
if (doReady) {
|
|
280
|
+
await spawnStatus(process.execPath, [BIN_PATH, "ready"]);
|
|
281
|
+
}
|
|
282
|
+
}
|