viewable-one 0.0.0-stage → 0.12.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.
- package/CHANGELOG.md +1483 -0
- package/LICENSE +21 -0
- package/README.md +738 -2
- package/SPAWN.md +2108 -0
- package/glytch.mjs +517 -0
- package/lib/acp.mjs +1018 -0
- package/lib/admission.mjs +105 -0
- package/lib/agent-selection.mjs +124 -0
- package/lib/agents.mjs +914 -0
- package/lib/atomic.mjs +96 -0
- package/lib/auth.mjs +1491 -0
- package/lib/authenticatedHttp.mjs +75 -0
- package/lib/boardside.mjs +225 -0
- package/lib/child.mjs +1292 -0
- package/lib/cli-output.mjs +176 -0
- package/lib/cloudflared-release.json +25 -0
- package/lib/cloudflared.mjs +81 -0
- package/lib/codex-rpc.mjs +92 -0
- package/lib/codex.mjs +767 -0
- package/lib/codexApps.mjs +344 -0
- package/lib/codexPlanWindow.mjs +84 -0
- package/lib/codexToolInputs.mjs +139 -0
- package/lib/codingChecks.mjs +55 -0
- package/lib/compose.mjs +76 -0
- package/lib/computerApps.mjs +110 -0
- package/lib/conversationRules.mjs +45 -0
- package/lib/detect.mjs +629 -0
- package/lib/devserver.mjs +670 -0
- package/lib/digest.mjs +290 -0
- package/lib/doctor.mjs +464 -0
- package/lib/exitClass.mjs +64 -0
- package/lib/hosted.mjs +112 -0
- package/lib/imageAttachments.mjs +31 -0
- package/lib/init-connect.mjs +418 -0
- package/lib/kill.mjs +104 -0
- package/lib/logindaemon.mjs +976 -0
- package/lib/logs.mjs +120 -0
- package/lib/mcpjson.mjs +242 -0
- package/lib/memberPreview.mjs +173 -0
- package/lib/multiwatch.mjs +428 -0
- package/lib/pagePictures.mjs +396 -0
- package/lib/pageRoutes.mjs +70 -0
- package/lib/paths.mjs +10 -0
- package/lib/permissionAsk.mjs +418 -0
- package/lib/picker.mjs +300 -0
- package/lib/platform.mjs +60 -0
- package/lib/previewGateway.mjs +130 -0
- package/lib/previewTunnel.mjs +85 -0
- package/lib/procs.mjs +428 -0
- package/lib/project-mcp.mjs +288 -0
- package/lib/projectPaths.mjs +31 -0
- package/lib/refusal.mjs +70 -0
- package/lib/registry.mjs +204 -0
- package/lib/repomap.mjs +323 -0
- package/lib/restartPass.mjs +151 -0
- package/lib/retired-address.mjs +78 -0
- package/lib/rowsAnswers.mjs +388 -0
- package/lib/rowsEvents.mjs +86 -0
- package/lib/run-engine.mjs +16 -0
- package/lib/runner-health.mjs +57 -0
- package/lib/runner.mjs +1640 -0
- package/lib/runnerAdmission.mjs +35 -0
- package/lib/runnerApi.mjs +312 -0
- package/lib/runnerCompletion.mjs +406 -0
- package/lib/runnerLease.mjs +37 -0
- package/lib/runnerMaintenance.mjs +27 -0
- package/lib/runnerPace.mjs +283 -0
- package/lib/runnerPerformance.mjs +26 -0
- package/lib/runnerRealtime.mjs +340 -0
- package/lib/runnerRegister.mjs +96 -0
- package/lib/runnerSettings.mjs +40 -0
- package/lib/runnerSettlement.mjs +145 -0
- package/lib/runnerWorkspace.mjs +177 -0
- package/lib/serverSweep.mjs +90 -0
- package/lib/sharedPreview.mjs +292 -0
- package/lib/signIn.mjs +47 -0
- package/lib/spawn.mjs +330 -0
- package/lib/sweep.mjs +161 -0
- package/lib/sync.mjs +484 -0
- package/lib/uninstall-project.mjs +589 -0
- package/lib/uninstall.mjs +464 -0
- package/lib/update.mjs +302 -0
- package/lib/usageLimit.mjs +108 -0
- package/lib/viewableFolder.mjs +105 -0
- package/lib/windows-command.mjs +85 -0
- package/lib/worktree.mjs +1142 -0
- package/lib/writePolicy.mjs +45 -0
- package/package.json +58 -4
- package/spawn-runner.mjs +167 -0
package/glytch.mjs
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `glytch` - one name for the whole thing, per ruling 2 of brief 19.
|
|
3
|
+
//
|
|
4
|
+
// A tester used to meet three bin names, PM_SPAWN_RUNNER, Prompting Machine,
|
|
5
|
+
// "the board" and `.glytch/` before running anything. This is the one
|
|
6
|
+
// command. One older bin, `pm-spawn-runner`, stays as an alias so nothing
|
|
7
|
+
// already installed has to move.
|
|
8
|
+
//
|
|
9
|
+
// glytch init connect this folder: register it, write .mcp.json,
|
|
10
|
+
// register with opencode, install the login daemon,
|
|
11
|
+
// and say what this machine has
|
|
12
|
+
// glytch login sign in to the hosted board over OAuth, so this
|
|
13
|
+
// machine's sessions reach it with no app open
|
|
14
|
+
// glytch logout forget the signed-in credential
|
|
15
|
+
// glytch doctor one list: every reason a session might not start
|
|
16
|
+
// glytch forget unregister this folder (the daemon keeps running)
|
|
17
|
+
// glytch stop stop the login daemon (its file stays on disk)
|
|
18
|
+
// glytch uninstall one command: every registered project, the login
|
|
19
|
+
// daemon, this machine's board grant, ~/.glytch
|
|
20
|
+
// glytch runner watch the spawn runner, over every registered folder
|
|
21
|
+
// glytch mcp retired (brief 30, W3) - prints where to go instead
|
|
22
|
+
// glytch --version
|
|
23
|
+
//
|
|
24
|
+
// `login` and `logout` are brief 30's (W2a): the local stdio MCP server is
|
|
25
|
+
// retired, and every agent - this one included - reaches the
|
|
26
|
+
// board through the hosted endpoint from here on
|
|
27
|
+
// (supabase/functions/board-mcp). `login` drives the same OAuth flow the
|
|
28
|
+
// endpoint already serves to claude.ai; see lib/auth.mjs.
|
|
29
|
+
//
|
|
30
|
+
// THE DISPATCHER DOES NOT REIMPLEMENT THE REMAINING OLD BIN. `runner` hands
|
|
31
|
+
// over to spawn-runner.mjs by rewriting argv and importing it, so there is
|
|
32
|
+
// one implementation and no second copy to drift. `mcp` USED TO hand over to
|
|
33
|
+
// the local server the same way; that server is deleted (W3), so `mcp` now
|
|
34
|
+
// just says so. The one place that is not a straight hand-over is `runner watch`
|
|
35
|
+
// with no PM_BOARD_ROOT: that is the multi-root daemon (lib/multiwatch.mjs),
|
|
36
|
+
// because launchd starts it standing nowhere.
|
|
37
|
+
|
|
38
|
+
import { fileURLToPath } from "node:url";
|
|
39
|
+
import { resolve } from "node:path";
|
|
40
|
+
import { homedir } from "node:os";
|
|
41
|
+
import { heading, checkSections, summary, homeNotice, wrap, bold, colorEnabled, dim, fail, hint, ok, step, warn, warnRow } from "./lib/cli-output.mjs";
|
|
42
|
+
|
|
43
|
+
import { login as hostedLogin, logout as hostedLogout } from "./lib/auth.mjs";
|
|
44
|
+
import { detectAgents, detectionLines, execProbe } from "./lib/detect.mjs";
|
|
45
|
+
import { allGreen, doctorRows } from "./lib/doctor.mjs";
|
|
46
|
+
import { loginDaemon, noDaemonInstall } from "./lib/logindaemon.mjs";
|
|
47
|
+
import { makeFileLogger } from "./lib/logs.mjs";
|
|
48
|
+
import { watchRegistered } from "./lib/multiwatch.mjs";
|
|
49
|
+
import { readRegistry, registryPath, removeFolder, touchRegistry } from "./lib/registry.mjs";
|
|
50
|
+
import { settingsFromEnv, newRunnerId, registerAndFindProject } from "./lib/runner.mjs";
|
|
51
|
+
import { syncFolder, syncRegistry } from "./lib/sync.mjs";
|
|
52
|
+
import { waitForRunnerHealth, writeRunnerHealth } from "./lib/runner-health.mjs";
|
|
53
|
+
import { initServiceScript, packageVersion, runUpdate } from "./lib/update.mjs";
|
|
54
|
+
import { initOptions, chooseAgents, readAgentSelection, saveAgentSelection } from "./lib/agent-selection.mjs";
|
|
55
|
+
import { projectMcpServerKey } from "./lib/project-mcp.mjs";
|
|
56
|
+
import { AGENT_CONNECT_HINT, agentLoginStep, personalBoardToken, signInStep } from "./lib/init-connect.mjs";
|
|
57
|
+
import { runUninstall, uninstallOptions } from "./lib/uninstall.mjs";
|
|
58
|
+
|
|
59
|
+
// THE SAME SENTENCE `SETUP_READY` NAMES (src/folder/runnerSetup.ts). Nothing
|
|
60
|
+
// under bridge/ may import from src/ (nor the reverse - see CLAUDE.md), so
|
|
61
|
+
// this is a second literal rather than a shared import; both sides are
|
|
62
|
+
// pinned by their own test (this package's init-connect.test.mjs and
|
|
63
|
+
// src/folder/runnerSetup.test.ts) so a change to one without the other fails
|
|
64
|
+
// a suite rather than silently drifting. `setupPromptText`'s stop condition
|
|
65
|
+
// - "When it prints that the runner is watching this folder, say
|
|
66
|
+
// 'Connected' and stop" - only ever fires if `init`'s own last lines
|
|
67
|
+
// literally say this (fix 2, 2026-09-19 persona walk: they never did).
|
|
68
|
+
export const RUNNER_WATCHING_LINE = "A runner is watching this folder.";
|
|
69
|
+
|
|
70
|
+
const say = (line = "") => process.stdout.write(`${line}\n`);
|
|
71
|
+
// BRIEF 31, MOVE 7: the FIRST line is red and the rest is left alone. An
|
|
72
|
+
// error someone pastes into an issue should still be readable, so the colour
|
|
73
|
+
// marks where it starts rather than painting the whole explanation.
|
|
74
|
+
const die = (line) => {
|
|
75
|
+
const color = colorEnabled({ isTTY: process.stderr.isTTY });
|
|
76
|
+
const [first, ...restLines] = String(line).split("\n");
|
|
77
|
+
process.stderr.write([fail(first, { color }), ...restLines].join("\n") + "\n");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
82
|
+
|
|
83
|
+
// BRIEF 31, MOVE 7: a function rather than a constant, because the command
|
|
84
|
+
// names are painted and painting is a decision about THIS terminal - a pipe
|
|
85
|
+
// gets plain text, the same rule every other line in this package follows.
|
|
86
|
+
// One example line under the quick start, nothing else: the usage text is a
|
|
87
|
+
// map, not a tutorial.
|
|
88
|
+
function usage() {
|
|
89
|
+
const color = colorEnabled();
|
|
90
|
+
const b = (text) => bold(text, { color });
|
|
91
|
+
return `viewable: connect a project folder to the Viewable board.
|
|
92
|
+
|
|
93
|
+
${b("viewable init")} Choose one or more coding agents for this project,
|
|
94
|
+
register their board connections, sign your computer in,
|
|
95
|
+
connect each agent, and set your computer to keep watching
|
|
96
|
+
without a terminal open: a LaunchAgent on macOS, a
|
|
97
|
+
scheduled task on Windows, a systemd user unit on WSL
|
|
98
|
+
and Linux. Safe to run twice.
|
|
99
|
+
${dim("Example: cd ~/Projects/your-project, then viewable init", { color })}
|
|
100
|
+
|
|
101
|
+
${b("viewable init --agents claude,codex")}
|
|
102
|
+
Select agents without a prompt (claude, opencode, codex).
|
|
103
|
+
${b("viewable init --yes")} Use saved choices, or all detected agents on first setup.
|
|
104
|
+
${b("viewable init --no-login")}
|
|
105
|
+
Do not sign your computer in; print the command instead.
|
|
106
|
+
${b("viewable init --no-agent-login")}
|
|
107
|
+
Do not run each agent's own OAuth verb; print it instead.
|
|
108
|
+
|
|
109
|
+
${b("viewable login")} Sign in to the hosted board over OAuth: opens a browser,
|
|
110
|
+
you approve, the credential lands in ~/.glytch/auth.json.
|
|
111
|
+
Every board tool your computer runs reaches the board
|
|
112
|
+
through it from here on: no app tab, no token to paste.
|
|
113
|
+
\`viewable init\` already signs your computer in if it is not
|
|
114
|
+
signed in; this is the command for re-auth.
|
|
115
|
+
|
|
116
|
+
${b("viewable logout")} Forget the signed-in credential (~/.glytch/auth.json), the
|
|
117
|
+
third of the three partial verbs below \`uninstall\`.
|
|
118
|
+
|
|
119
|
+
${b("viewable update")} Install the latest published version and restart your
|
|
120
|
+
computer's connection. Keeps folders and sign-in.
|
|
121
|
+
Never runs automatically. Never needs sudo.
|
|
122
|
+
|
|
123
|
+
${b("viewable doctor")} One list: folder registered, your computer connected and
|
|
124
|
+
running, mirror fresh, agents found, Claude Code logged in,
|
|
125
|
+
signed in to the board, version against npm. Exits 0
|
|
126
|
+
when no checks fail.
|
|
127
|
+
|
|
128
|
+
${b("viewable forget")} Stop watching this folder, one of the three partial verbs
|
|
129
|
+
below \`uninstall\`. Your computer keeps watching the others.
|
|
130
|
+
|
|
131
|
+
${b("viewable stop")} Stop watching folders. The file it runs from stays on
|
|
132
|
+
disk, so \`viewable init\` brings it back without a reinstall.
|
|
133
|
+
Another of the three partial verbs below \`uninstall\`.
|
|
134
|
+
|
|
135
|
+
${b("viewable uninstall")} Remove Viewable, its board grant and everything it wrote:
|
|
136
|
+
one command instead of \`forget\`, \`stop\` and \`logout\` run by
|
|
137
|
+
hand. Every registered project (or just this one, with
|
|
138
|
+
\`--project\`), then the background watcher, the board
|
|
139
|
+
sign-in, and ~/.glytch itself. Confirms once; \`--yes\` skips
|
|
140
|
+
the prompt, \`--dry-run\` reports what would happen and
|
|
141
|
+
changes nothing, \`--keep-login\` leaves your computer signed
|
|
142
|
+
in.
|
|
143
|
+
|
|
144
|
+
${b("viewable runner")} ... The spawn runner (\`pm-spawn-runner\` is the same thing).
|
|
145
|
+
\`watch\` with no PM_BOARD_ROOT watches every registered
|
|
146
|
+
folder; with it set, it watches that one.
|
|
147
|
+
|
|
148
|
+
${b("viewable mcp")} Retired (brief 30): the local MCP server is gone. Run
|
|
149
|
+
\`viewable init\` and sign in with \`viewable login\` instead:
|
|
150
|
+
every agent reaches the board over the hosted endpoint.
|
|
151
|
+
|
|
152
|
+
${b("viewable --version")}
|
|
153
|
+
`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Hand over to the spawn-runner bin, unchanged (its only remaining caller
|
|
157
|
+
* since `mcp` stopped handing over to the local server, W3). argv is rewritten
|
|
158
|
+
* first because the script reads process.argv at module scope, which is
|
|
159
|
+
* exactly what makes this a hand-over rather than a copy. */
|
|
160
|
+
async function handOver(script, args) {
|
|
161
|
+
// THE IMPORT TAKES THE URL, THE ARGV TAKES THE PATH (2026-09-07, brief 21,
|
|
162
|
+
// the first windows-latest run). `import()` of a bare absolute path works on
|
|
163
|
+
// POSIX only by accident of `/` also starting a valid specifier: on Windows
|
|
164
|
+
// `D:\...\spawn-runner.mjs` is read as the URL scheme `d:` and the ESM
|
|
165
|
+
// loader refuses it (ERR_UNSUPPORTED_ESM_URL_SCHEME), so `glytch runner` and
|
|
166
|
+
// `glytch mcp` - both hand-overs, and between them most of what the package
|
|
167
|
+
// does - died before reaching either script. process.argv still gets the
|
|
168
|
+
// native path, because that is what both scripts read.
|
|
169
|
+
const url = new URL(`./${script}`, import.meta.url);
|
|
170
|
+
process.argv = [process.argv[0], fileURLToPath(url), ...args];
|
|
171
|
+
await import(url.href);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// A NUMBERED WALK SINCE BRIEF 31 (Moves 1, 2 and 7, 2026-09-12). Four steps,
|
|
175
|
+
// each with its rows under it: choose the agents, connect the project, sign
|
|
176
|
+
// this machine in, connect each agent. The last two used to be printed
|
|
177
|
+
// instructions - "now run glytch login", "now run codex mcp login board" -
|
|
178
|
+
// and printing them was four of the six steps a new machine took. They are
|
|
179
|
+
// actions now; lib/init-connect.mjs is both of them, injectable end to end.
|
|
180
|
+
const INIT_STEPS = 4;
|
|
181
|
+
|
|
182
|
+
async function init() {
|
|
183
|
+
const options = initOptions(rest);
|
|
184
|
+
const color = colorEnabled();
|
|
185
|
+
const paint = { color };
|
|
186
|
+
const root = resolve(process.cwd());
|
|
187
|
+
const checkStartedAt = Date.now();
|
|
188
|
+
const disabled = noDaemonInstall(process.env);
|
|
189
|
+
// The service runs the script verification proved, not this process's own
|
|
190
|
+
// file, which may be a same-version copy about to be deleted (#600). No log:
|
|
191
|
+
// the PATH diagnostic is noise as init's first line.
|
|
192
|
+
const service = await initServiceScript();
|
|
193
|
+
if (!service.ok) { say(service.error); return 1; }
|
|
194
|
+
const saved = await readAgentSelection(root);
|
|
195
|
+
say(heading(`Viewable ${packageVersion()} · Project setup`, paint));
|
|
196
|
+
say(`Folder: ${root}`);
|
|
197
|
+
for (const line of homeNotice(root, homedir(), paint)) say(line);
|
|
198
|
+
|
|
199
|
+
say("");
|
|
200
|
+
say(step(1, INIT_STEPS, "Choose coding agents", paint));
|
|
201
|
+
const agents = await detectAgents();
|
|
202
|
+
const selected = await chooseAgents({ options, saved, found: agents });
|
|
203
|
+
// Save before registry registration: the daemon can notice a new folder
|
|
204
|
+
// immediately and must never race init with the legacy all-agents setup.
|
|
205
|
+
// AN EMPTY SELECTION (fix 3, 2026-09-19: `--yes` with no agent binary on
|
|
206
|
+
// PATH and no saved choice) is deliberate, not a config to persist -
|
|
207
|
+
// `saveAgentSelection([])` would write a file `readAgentSelection` then
|
|
208
|
+
// refuses to read back, since `parseAgentSelection` treats empty as
|
|
209
|
+
// invalid. Nothing is saved, so the next run tries detection again rather
|
|
210
|
+
// than remembering "no agents" forever.
|
|
211
|
+
if (selected.length) await saveAgentSelection(root, selected);
|
|
212
|
+
say(heading("Installed agents", paint));
|
|
213
|
+
for (const line of detectionLines(agents)) for (const part of wrap(line, 88, " ")) say(part);
|
|
214
|
+
say("");
|
|
215
|
+
say(`Board connections: ${selected.length ? selected.join(", ") : "none"}`);
|
|
216
|
+
if (selected.includes("claude") && agents.claude?.found && agents.claude.loggedIn === false) {
|
|
217
|
+
say("");
|
|
218
|
+
say("Claude Code is installed but not logged in. Run `claude` once and log in,");
|
|
219
|
+
say("or a session will fail the moment it spawns.");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
say("");
|
|
223
|
+
say(step(2, INIT_STEPS, "Connect this project", paint));
|
|
224
|
+
const rows = await syncFolder(root, { agents, selectedAgents: selected, exec: execProbe, ensureVersion: true, scriptPath: service.scriptPath, deferRegistry: true });
|
|
225
|
+
// NO AGENT WAS FOUND TO SELECT (fix 3): a warn row, in the same "Coding
|
|
226
|
+
// agents" group lib/doctor.mjs's own "coding agents" row lands in
|
|
227
|
+
// (checkSections' groups keys on "agents"), rather than the throw this
|
|
228
|
+
// used to be. `warn` because the runner still connects to the board fine
|
|
229
|
+
// with no agent chosen yet - `rows.every(r => r.ok)` below stays green.
|
|
230
|
+
if (!selected.length) {
|
|
231
|
+
rows.push(warnRow("agents", "coding agents", "none", "No coding agent was found on PATH. Install Claude Code, Codex or OpenCode, then run `viewable init --agents <name>` to connect it."));
|
|
232
|
+
}
|
|
233
|
+
for (const line of checkSections(rows, paint)) say(line);
|
|
234
|
+
|
|
235
|
+
say("");
|
|
236
|
+
say(step(3, INIT_STEPS, "Sign your computer in", paint));
|
|
237
|
+
// Both steps print their own rows as they settle - a row is the answer to
|
|
238
|
+
// the browser window that was just open, and it belongs under it.
|
|
239
|
+
const machine = await signInStep({ skip: !options.login, log: say, color });
|
|
240
|
+
// --no-login skips saving credentials, not use of a token already held.
|
|
241
|
+
// Read once here and reuse it for the agents' own configuration below.
|
|
242
|
+
const token = await personalBoardToken({ env: process.env });
|
|
243
|
+
// Restore deliberately before publishing a new local registry entry. A
|
|
244
|
+
// watcher must not see the folder while the server still wants it removed.
|
|
245
|
+
if (machine?.status === "ok" || token) {
|
|
246
|
+
const registered = await registerAndFindProject(root, {
|
|
247
|
+
...settingsFromEnv(), runnerId: newRunnerId(), markExtra: () => ({ version: packageVersion() }),
|
|
248
|
+
}, { readd: true });
|
|
249
|
+
if (!registered.ok) {
|
|
250
|
+
say(registered.error);
|
|
251
|
+
// Keep ordinary setup usable offline, as before. Only a known removal
|
|
252
|
+
// requires a successful restore before publishing the local folder.
|
|
253
|
+
if (registered.readdRequired) return 1;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const registryRow = await syncRegistry(root);
|
|
257
|
+
rows.unshift(registryRow);
|
|
258
|
+
for (const line of checkSections([registryRow], paint)) say(line);
|
|
259
|
+
|
|
260
|
+
say("");
|
|
261
|
+
say(step(4, INIT_STEPS, "Connect each agent", paint));
|
|
262
|
+
const connected = await agentLoginStep({
|
|
263
|
+
root,
|
|
264
|
+
selected,
|
|
265
|
+
agents,
|
|
266
|
+
exec: execProbe,
|
|
267
|
+
skip: !options.agentLogin,
|
|
268
|
+
token,
|
|
269
|
+
log: say,
|
|
270
|
+
color,
|
|
271
|
+
});
|
|
272
|
+
if (!connected.length) say(dim(" No selected agent is installed on your computer.", paint));
|
|
273
|
+
|
|
274
|
+
say("");
|
|
275
|
+
say(heading("Next steps", paint));
|
|
276
|
+
const next = (text) => { for (const line of wrap(text, 88, " ")) say(line); };
|
|
277
|
+
next("The coding agent is chosen in the app, on the board.");
|
|
278
|
+
// STEP 4 OWNS SIGN-IN (2026-09-12, reviewing the merged output). This block
|
|
279
|
+
// used to repeat lib/doctor.mjs's BOARD_SIGN_IN line for every agent step 4
|
|
280
|
+
// had not finished - which is every agent step 4 already printed the verb
|
|
281
|
+
// for, as a hint directly under its own row, one screen higher. Two
|
|
282
|
+
// spellings of the same instruction in one run is one too many, and the
|
|
283
|
+
// Claude one was long enough that it went out unwrapped. `doctor` still
|
|
284
|
+
// gives the line later, for a machine that never ran init or skipped it.
|
|
285
|
+
// Below the version floor the /mcp fallback rides step 4's own row
|
|
286
|
+
// (lib/init-connect.mjs), which is where it belongs: beside the reason.
|
|
287
|
+
if (selected.includes("codex")) {
|
|
288
|
+
next("Codex: trust this project and restart its MCP connection to load the tools.");
|
|
289
|
+
next("Codex can run board ideas and chat through the runner using your signed-in account.");
|
|
290
|
+
}
|
|
291
|
+
if (saved?.some((agent) => !selected.includes(agent))) {
|
|
292
|
+
next("Previously configured agents were left in place; init and doctor now maintain only this selection.");
|
|
293
|
+
}
|
|
294
|
+
say("");
|
|
295
|
+
// NOT "link the project folder" any more (2026-09-26): a coding run needs
|
|
296
|
+
// only the project this command just registered; linking the folder in
|
|
297
|
+
// the app shows its files on the board and nothing else.
|
|
298
|
+
say(bold("Open the board and run an idea on your coding agent.", paint));
|
|
299
|
+
say(dim("Linking this folder in the app is optional; it only shows its files on the board.", paint));
|
|
300
|
+
say("");
|
|
301
|
+
say(summary(rows, paint));
|
|
302
|
+
const allOk = rows.every((r) => r.ok);
|
|
303
|
+
// YOUR COMPUTER HAS TO BE SIGNED IN FOR A RUNNER TO WATCH (#324). A sign-in
|
|
304
|
+
// that failed, or was skipped for want of a terminal, left the daemon
|
|
305
|
+
// nothing to reach the board with, so the folder is not watched and the
|
|
306
|
+
// command exits 1. `--no-login` is a choice (a CI box): it keeps exit 0,
|
|
307
|
+
// but nothing is watching there either, so the line is not printed.
|
|
308
|
+
const signedIn = machine?.status === "ok";
|
|
309
|
+
const signInChosenOff = !options.login && machine?.status === "warn";
|
|
310
|
+
// FIX 2, THE STOP SENTENCE (2026-09-19 persona walk): `setupPromptText`
|
|
311
|
+
// (src/folder/runnerSetup.ts) tells the pasting agent to stop once init
|
|
312
|
+
// "prints that the runner is watching this folder" - a phrase this command
|
|
313
|
+
// never actually printed. It is the true last line now, and only when
|
|
314
|
+
// every row landed and your computer is signed in - a failed check is not
|
|
315
|
+
// a watching runner, and neither is a computer the board cannot reach.
|
|
316
|
+
let healthy = false;
|
|
317
|
+
if (allOk && signedIn && !disabled) {
|
|
318
|
+
// A daemon already watching this folder beats on a registry rewrite.
|
|
319
|
+
await touchRegistry().catch(() => {});
|
|
320
|
+
const result = await waitForRunnerHealth({
|
|
321
|
+
version: packageVersion(), after: checkStartedAt, root, requireStartedAfter: false,
|
|
322
|
+
});
|
|
323
|
+
healthy = result.ok;
|
|
324
|
+
if (healthy) {
|
|
325
|
+
await writeRunnerHealth({ version: packageVersion(), setupId: process.env.VIEWABLE_SETUP_ID ?? process.env.GLYTCH_SETUP_ID ?? "manual" }, process.env, "cli");
|
|
326
|
+
say(RUNNER_WATCHING_LINE);
|
|
327
|
+
} else say(result.error);
|
|
328
|
+
}
|
|
329
|
+
// FIX 1B, THE FALLBACK SENTENCE: an agent step 4 could not connect over
|
|
330
|
+
// the hosted endpoint (no terminal, and 1a either did not apply to it or
|
|
331
|
+
// did not land) gets ONE more actionable line, un-buried, after
|
|
332
|
+
// everything else - not the warn row a few screens up that a paste-and-run
|
|
333
|
+
// agent has no reason to scroll back to.
|
|
334
|
+
for (const row of connected) {
|
|
335
|
+
if (row.status === "warn" && row.text === "skipped (no terminal)") {
|
|
336
|
+
const serverKey = await projectMcpServerKey(root, row.agent);
|
|
337
|
+
say(AGENT_CONNECT_HINT[row.agent]?.replace(/viewable/g, serverKey) ?? `Sign in to Viewable from ${row.label}.`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return allOk && (signInChosenOff || (signedIn && (disabled || healthy))) ? 0 : 1;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// THE PAINT RULE FOR EVERY COMMAND BELOW (brief 31, Move 7): a heading, then
|
|
344
|
+
// the RESULT in green when it landed and yellow when nothing happened, then
|
|
345
|
+
// the plain detail lines. `doctor` keeps its own shape - its rows already are
|
|
346
|
+
// this pattern and they are the pattern the rest was built to match.
|
|
347
|
+
async function login() {
|
|
348
|
+
const color = colorEnabled();
|
|
349
|
+
say(heading("Viewable · Sign in", { color }));
|
|
350
|
+
say(bold("Viewable needs a browser sign-in for your computer.", { color }));
|
|
351
|
+
const stored = await hostedLogin({ log: (line) => say(hint(line, { color })) });
|
|
352
|
+
say("");
|
|
353
|
+
say(ok(`Signed in to ${stored.appUrl}.`, { color }));
|
|
354
|
+
say(`Board tools on your computer now reach ${stored.mcpUrl}: no app tab, no token to paste.`);
|
|
355
|
+
say(`Saved to ~/.glytch/auth.json (mode 0600), or GLYTCH_HOME when that is set.`);
|
|
356
|
+
return 0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function logout() {
|
|
360
|
+
const color = colorEnabled();
|
|
361
|
+
say(heading("Viewable · Sign out", { color }));
|
|
362
|
+
const removed = await hostedLogout();
|
|
363
|
+
say(removed ? ok("Signed out. ~/.glytch/auth.json removed.", { color }) : warn("Not signed in, nothing to remove.", { color }));
|
|
364
|
+
return 0;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function forget() {
|
|
368
|
+
const color = colorEnabled();
|
|
369
|
+
const root = resolve(process.cwd());
|
|
370
|
+
say(heading("Viewable · Forget project", { color }));
|
|
371
|
+
const { removed } = await removeFolder(root);
|
|
372
|
+
say(
|
|
373
|
+
removed
|
|
374
|
+
? ok(`Forgot ${root}. Your computer keeps watching the ${(await readRegistry()).length} folder(s) left in ${registryPath()}.`, { color })
|
|
375
|
+
: warn(`${root} was not registered, so there is nothing to forget.`, { color }),
|
|
376
|
+
);
|
|
377
|
+
say("Nothing was deleted inside the folder: .mcp.json, .glytch/ and every session it holds are still there.");
|
|
378
|
+
return 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// WHATEVER THIS MACHINE INSTALLS (2026-09-07, brief 21 session 4): a
|
|
382
|
+
// LaunchAgent on macOS, a Task Scheduler onlogon task on Windows, a systemd
|
|
383
|
+
// user unit on WSL and Linux. `stop` used to name launchctl directly.
|
|
384
|
+
async function stop() {
|
|
385
|
+
const color = colorEnabled();
|
|
386
|
+
const daemon = loginDaemon();
|
|
387
|
+
say(heading("Viewable · Stop runner", { color }));
|
|
388
|
+
const result = await daemon.stop();
|
|
389
|
+
if (result.unloaded) {
|
|
390
|
+
say(ok(`Unloaded the ${daemon.rowLabel} (${result.file}).`, { color }));
|
|
391
|
+
say(`The ${daemon.kind === "launchd" ? "plist" : "file"} stays on disk, so \`viewable init\` starts it again without a reinstall.`);
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
say(fail(`Could not unload the ${daemon.rowLabel}: ${result.why}`, { color }));
|
|
395
|
+
say(`Expected it at ${daemon.filePath()}.`);
|
|
396
|
+
return 1;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// THE RULING, 2026-09-12: removing glytch, its OAuth grants and
|
|
400
|
+
// everything it wrote must be one straightforward command for any user.
|
|
401
|
+
// Everything the four steps do lives in lib/uninstall.mjs, injectable end to
|
|
402
|
+
// end; this is the thin CLI wiring, same as `init`'s relationship to
|
|
403
|
+
// lib/init-connect.mjs.
|
|
404
|
+
async function uninstall() {
|
|
405
|
+
const options = uninstallOptions(rest);
|
|
406
|
+
return runUninstall({ root: resolve(process.cwd()), options, log: say });
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function doctor() {
|
|
410
|
+
const color = colorEnabled();
|
|
411
|
+
const root = resolve(process.cwd());
|
|
412
|
+
say(heading(`Viewable ${packageVersion()} · Health check`, { color }));
|
|
413
|
+
say(`Folder: ${root}`);
|
|
414
|
+
for (const line of homeNotice(root, homedir(), { color })) say(line);
|
|
415
|
+
const rows = await doctorRows(root);
|
|
416
|
+
for (const line of checkSections(rows, { color })) say(line);
|
|
417
|
+
say("");
|
|
418
|
+
const green = allGreen(rows);
|
|
419
|
+
say(summary(rows, { color }));
|
|
420
|
+
return green ? 0 : 1;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
async function runner(args) {
|
|
424
|
+
// With PM_BOARD_ROOT set the behaviour is exactly today's, down to the same
|
|
425
|
+
// process: hand straight over.
|
|
426
|
+
const explicitRoot = process.env.PM_BOARD_ROOT?.trim();
|
|
427
|
+
if (args[0] !== "watch" || explicitRoot) {
|
|
428
|
+
await handOver("spawn-runner.mjs", args);
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const settings = { ...settingsFromEnv(), runnerId: newRunnerId() };
|
|
432
|
+
if (!settings.enabled) {
|
|
433
|
+
die(
|
|
434
|
+
"The runner is off.\n\n" +
|
|
435
|
+
"It has no default-on path and it does not start itself: a session\n" +
|
|
436
|
+
"uses your own Claude Code or Codex plan, so an absent variable is not consent. Your computer\n" +
|
|
437
|
+
"turns it on for itself once `viewable init` sets it up; to run one by hand:\n\n" +
|
|
438
|
+
" PM_SPAWN_RUNNER=on viewable runner watch\n",
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
const log = makeFileLogger();
|
|
442
|
+
const controller = new AbortController();
|
|
443
|
+
process.on("SIGINT", () => {
|
|
444
|
+
log("stopping. Any session already running keeps going; it is a separate process.");
|
|
445
|
+
controller.abort();
|
|
446
|
+
});
|
|
447
|
+
await watchRegistered({ settings, signal: controller.signal, log });
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function main() {
|
|
452
|
+
switch (command) {
|
|
453
|
+
case "update": {
|
|
454
|
+
const result = await runUpdate({ log: say });
|
|
455
|
+
if (!result.ok) say(result.error);
|
|
456
|
+
return result.ok ? 0 : 1;
|
|
457
|
+
}
|
|
458
|
+
case "init":
|
|
459
|
+
return init();
|
|
460
|
+
case "login":
|
|
461
|
+
return login();
|
|
462
|
+
case "logout":
|
|
463
|
+
return logout();
|
|
464
|
+
case "forget":
|
|
465
|
+
return forget();
|
|
466
|
+
case "stop":
|
|
467
|
+
return stop();
|
|
468
|
+
case "uninstall":
|
|
469
|
+
return uninstall();
|
|
470
|
+
case "doctor":
|
|
471
|
+
return doctor();
|
|
472
|
+
case "runner":
|
|
473
|
+
return runner(rest);
|
|
474
|
+
case "mcp":
|
|
475
|
+
// Retired 2026-09-11 (brief 30, W3): the local stdio MCP server this
|
|
476
|
+
// used to hand over to is deleted. Every agent reaches
|
|
477
|
+
// the board over the hosted endpoint instead.
|
|
478
|
+
say(heading("Viewable · mcp (retired)", { color: colorEnabled() }));
|
|
479
|
+
say(warn("The local board MCP server is retired.", { color: colorEnabled() }));
|
|
480
|
+
say("Run `viewable init` to connect this folder: it signs your computer in and connects each agent over the hosted endpoint from there.");
|
|
481
|
+
return 1;
|
|
482
|
+
case "--version":
|
|
483
|
+
case "-v":
|
|
484
|
+
case "version":
|
|
485
|
+
say(packageVersion());
|
|
486
|
+
return 0;
|
|
487
|
+
default:
|
|
488
|
+
say(usage());
|
|
489
|
+
return command === undefined || command === "help" || command === "--help" ? 0 : 1;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// These commands are done producing output the moment `main()` resolves;
|
|
494
|
+
// unlike `runner` they never own the process. But anything a command ran
|
|
495
|
+
// along the way can leave a stream flowing - the arrow-key picker in
|
|
496
|
+
// `init` adds a `data` listener to stdin - and a flowing stream keeps the
|
|
497
|
+
// event loop alive even with picker.mjs's own cleanup in place. Seen for
|
|
498
|
+
// real 2026-09-12: `init` printed "All checks passed." and then hung until
|
|
499
|
+
// Ctrl-C. Belt and braces: flush stdout, then exit explicitly instead of
|
|
500
|
+
// trusting Node to notice there is nothing left to do.
|
|
501
|
+
//
|
|
502
|
+
// P4 (review 2026-09-12): this used to be a hand-maintained set of command
|
|
503
|
+
// names, one more thing to remember to update for every command added
|
|
504
|
+
// after it - and the picker bug above is exactly the kind of thing a
|
|
505
|
+
// forgotten entry would silently bring back for a leaf command nobody
|
|
506
|
+
// thought to add. Every numeric result now exits explicitly; only `runner`
|
|
507
|
+
// watch's null (it owns the process) skips it, with no list to fall out of
|
|
508
|
+
// date.
|
|
509
|
+
main()
|
|
510
|
+
.then((code) => {
|
|
511
|
+
// null means "this command owns the process now" - the runner watches
|
|
512
|
+
// until it is stopped, and the MCP server serves stdio for ever.
|
|
513
|
+
if (typeof code !== "number") return;
|
|
514
|
+
if (code !== 0) process.exitCode = code;
|
|
515
|
+
process.stdout.write("", () => process.exit(code));
|
|
516
|
+
})
|
|
517
|
+
.catch((error) => die(error instanceof Error ? error.message : String(error)));
|