@winding-labs/glow 0.1.1 → 0.1.5
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 +63 -7
- package/SECURITY.md +48 -2
- package/bin/glow.mjs +280 -39
- package/lib/agents.mjs +42 -0
- package/lib/core.mjs +165 -3
- package/lib/hook.mjs +14 -7
- package/lib/runner.mjs +669 -0
- package/marketplace/.agents/plugins/marketplace.json +12 -0
- package/marketplace/.claude-plugin/marketplace.json +13 -0
- package/marketplace/glow/.claude-plugin/plugin.json +10 -0
- package/marketplace/glow/.codex-plugin/plugin.json +22 -0
- package/marketplace/glow/skills/ignore/SKILL.md +15 -0
- package/marketplace/glow/skills/run/SKILL.md +26 -0
- package/marketplace/glow/skills/status/SKILL.md +15 -0
- package/marketplace/glow/skills/unignore/SKILL.md +16 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -15,11 +15,19 @@ branch, commits and pull request. From that post in Glow you can send the sessio
|
|
|
15
15
|
- **Only to Glow** (`https://app.meetglow.ai`). The address is fixed when you set up and can't be changed by a repo or
|
|
16
16
|
an environment variable (the hook runs with an empty environment but `HOME` and `PATH`).
|
|
17
17
|
- **Only from linked repos.** A session in any other repo sends nothing. You link repos to products in Glow.
|
|
18
|
-
- **
|
|
19
|
-
|
|
20
|
-
-
|
|
18
|
+
- **Never from a repo you ignore.** `/glow:ignore` in a repo (or `npx @winding-labs/glow ignore`) and its sessions send
|
|
19
|
+
nothing from this computer, even if it's linked: the hook stops before it makes any request.
|
|
20
|
+
- **Only the agent's last reply and the request it answers**, with keys and tokens masked, plus the branch, the titles
|
|
21
|
+
of the branch's commits and a pull request link on the same repo, and which of GitHub (`gh` signed in), Chrome, Docker
|
|
22
|
+
and Xcode this computer has (yes or no each, for its tags in Glow). Never your code, files or the transcript: the hook
|
|
23
|
+
reads only your latest request from the agent's own session file.
|
|
24
|
+
- Glow turns the reply into a short summary right away and **doesn't keep the reply or the request**. From the request
|
|
25
|
+
it keeps only the recipe it names when it's a job you'd run again ("Pen test the API", "Optimize SEO"), so it can
|
|
26
|
+
count how often you run it and offer to run it for you. Don't want that? `npx @winding-labs/glow recipes off`.
|
|
21
27
|
- This computer's key can do three things: send a report, read which repos are linked, and fetch messages you sent to
|
|
22
|
-
its sessions.
|
|
28
|
+
its sessions. While `glow run` is on, it also picks up the sessions you start here and says how they're going (the
|
|
29
|
+
repo, the last command or file, masked; never output or code). It can't read anything else in your workspace. It
|
|
30
|
+
stops working after 90 days unused.
|
|
23
31
|
|
|
24
32
|
## Messages back
|
|
25
33
|
|
|
@@ -27,19 +35,67 @@ Only the person who paired the computer can message its sessions, and Glow asks
|
|
|
27
35
|
first. Claude Code picks a message up while it's idle (for 25 minutes after its last reply); Codex picks it up when its
|
|
28
36
|
current turn ends. Don't want that? `npx @winding-labs/glow messages off` on the computer.
|
|
29
37
|
|
|
38
|
+
## Start sessions from Glow
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
npx @winding-labs/glow run ~/code
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Only if you want it, and only while it runs: Glow's Agents tab can then start a Claude Code session on this computer,
|
|
45
|
+
from a queued task ("Run on …") or a prompt you type ("New session"). It runs in your checkout of a repo that's linked
|
|
46
|
+
to a product and found under the folders you give it (the current folder by default), and the Agents tab shows it live:
|
|
47
|
+
the repo, what it's on, its status, how long it's been running, its last command or tool, and a Stop button. Its reply
|
|
48
|
+
posts to the product's channel like any session's, except for a task someone else wrote or one filed from a capture:
|
|
49
|
+
then only you see it, under the session.
|
|
50
|
+
|
|
51
|
+
- **Only you** (the person who paired the computer) can start one, and Glow asks for Face ID or your passkey first.
|
|
52
|
+
- **A mode you pick each time:** *Look only* (Claude Code's plan mode: it reads, changes nothing), *Edit files* (also
|
|
53
|
+
edits files in the repo) or *Auto* (Claude Code's auto mode; never for a queued task). Never a mode that skips
|
|
54
|
+
permission checks. Opus at medium effort unless you pick another model or effort.
|
|
55
|
+
- **This computer decides too**, whatever Glow sends: `--max-mode read|edit|auto` is the most a session may do here
|
|
56
|
+
(*Look only* by default: `--max-mode edit` lets sessions edit files, `--max-mode auto` allows *Auto*; the app offers
|
|
57
|
+
only what the computer allows); `--repo owner/name` (repeat it) runs only those repos,
|
|
58
|
+
and `--repo owner/name=~/code/zest` picks the checkout when a repo is checked out more than once (otherwise it doesn't
|
|
59
|
+
run; worktrees don't count: it runs in the main checkout). Only in a folder you've trusted in Claude Code (opened `claude` there and said yes), since its own `.claude`
|
|
60
|
+
settings and hooks apply. A session id is run once, never again.
|
|
61
|
+
- **Up to 3 at once** on a computer, one per repo, 30 a day, 3 hours each at most. Stop from Glow ends the session and
|
|
62
|
+
the commands it started. Pausing the computer in Glow stops them all; Ctrl-C (or closing the terminal) stops the
|
|
63
|
+
runner and its sessions, and everything they started.
|
|
64
|
+
- `run --background` keeps it going after you close the terminal (log: `~/.glow/runner.log`); `run stop` stops it. A
|
|
65
|
+
session the runner started can't stop it or turn Glow off.
|
|
66
|
+
|
|
67
|
+
## In Claude Code and Codex
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
/glow:status what this folder sends (and why not), what's set up, which repos report or are ignored
|
|
71
|
+
/glow:ignore this repo never sends to Glow from this computer (add a folder or owner/name for another)
|
|
72
|
+
/glow:unignore let it send again (only when you type it: an agent can't run it on its own in Claude Code)
|
|
73
|
+
/glow:run start (or stop) the runner in the background, for the linked repos under the session's folder
|
|
74
|
+
(only when you type it; Look only: --max-mode and --repo are for the terminal)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
In Codex they're `$glow:status`, `$glow:ignore`, `$glow:unignore` and `$glow:run`. Setup adds them as a `glow` plugin, through each
|
|
78
|
+
agent's own `plugin` command; each may run only its own `~/.glow/glow` command, and a session Glow started can't run
|
|
79
|
+
any of them.
|
|
80
|
+
|
|
30
81
|
## Commands
|
|
31
82
|
|
|
32
83
|
```
|
|
33
84
|
npx @winding-labs/glow set up (or add an agent you installed later)
|
|
34
|
-
npx @winding-labs/glow status what's set up
|
|
85
|
+
npx @winding-labs/glow status what this folder sends, what's set up, which repos report
|
|
86
|
+
npx @winding-labs/glow ignore [repo] this repo (or a folder, or owner/name) never sends from this computer
|
|
87
|
+
npx @winding-labs/glow unignore [repo] let it send again
|
|
35
88
|
npx @winding-labs/glow off | on stop or start sending from this computer
|
|
36
89
|
npx @winding-labs/glow messages off messages from Glow no longer reach your agents here (messages on: again)
|
|
37
|
-
npx @winding-labs/glow
|
|
90
|
+
npx @winding-labs/glow run [folders] let Glow start Claude Code sessions in the linked repos under these folders
|
|
91
|
+
[--max-mode read|edit|auto] [--repo owner/name[=path]…] [--background]
|
|
92
|
+
npx @winding-labs/glow run stop stop the runner (and its sessions)
|
|
93
|
+
npx @winding-labs/glow uninstall remove the hooks and the plugin, revoke the key, delete ~/.glow
|
|
38
94
|
```
|
|
39
95
|
|
|
40
96
|
Codex asks before it runs any new hook: open Codex and run `/hooks` once to trust Glow's.
|
|
41
97
|
|
|
42
|
-
Files: `~/.glow/` (the key,
|
|
98
|
+
Files: `~/.glow/` (owner-only: the key, the ignored repos, a copy of this package and the plugin), a `Stop` hook in `~/.claude/settings.json` and `~/.codex/hooks.json` (a
|
|
43
99
|
symlinked settings file stays a link). The first time, the originals are copied to `*.before-glow`.
|
|
44
100
|
|
|
45
101
|
Security: [SECURITY.md](SECURITY.md).
|
package/SECURITY.md
CHANGED
|
@@ -7,12 +7,48 @@ Report a vulnerability to **security@meetglow.ai**. Please don't open a public i
|
|
|
7
7
|
- **Talk to one host:** `https://app.meetglow.ai`, fixed at setup in `~/.glow/config.json` (owner-only) and checked on
|
|
8
8
|
every call. Redirects are refused. The hook runs with an empty environment except `HOME` and `PATH`, so no proxy, TLS
|
|
9
9
|
or `NODE_*` setting can send it anywhere else.
|
|
10
|
-
- **Send only from repos you linked** to a product in Glow: the agent's last reply
|
|
11
|
-
|
|
10
|
+
- **Send only from repos you linked** to a product in Glow: the agent's last reply and the request it answers (keys and
|
|
11
|
+
tokens masked; the request only from a .jsonl under ~/.claude/projects or ~/.codex/sessions, never with `recipes
|
|
12
|
+
off`), the branch, up to five commit titles on it, and a pull request link on the same repo. Never code, files or
|
|
13
|
+
transcripts.
|
|
14
|
+
A repo you ignore here (`/glow:ignore`) sends nothing, linked or not: the hook stops before any request.
|
|
12
15
|
- **Run git read-only** (the origin URL, the branch, commit titles), with no shell, hooks, fsmonitor, pager, signature
|
|
13
16
|
checks or `GIT_*` variables, so a repo's `.git/config` can't make it run a program.
|
|
14
17
|
- **Edit two settings files:** add one `Stop` hook to `~/.claude/settings.json` and `~/.codex/hooks.json`, keep
|
|
15
18
|
everything else, back up the originals once (`*.before-glow`), and never overwrite a file that isn't valid JSON.
|
|
19
|
+
- **Add one plugin** (`glow`, from `~/.glow/marketplace`) with each agent's own `claude plugin` / `codex plugin`
|
|
20
|
+
command. Each may run only its own `~/.glow/glow` command (`status`, `ignore …`, `unignore …`; `/glow:run`: exactly
|
|
21
|
+
`run --background`, `run stop` or `status`), and `/glow:unignore` and `/glow:run` run only when you type them.
|
|
22
|
+
|
|
23
|
+
- **Start Claude Code, only while `glow run` runs** (you start it; nothing installs it to start on its own). It runs
|
|
24
|
+
`claude` as an argument list (no shell), in your checkout of a linked repo found under the folders you gave it, with
|
|
25
|
+
the prompt on stdin (so a prompt can't become a flag), in the mode picked in Glow: `plan` for *Look only* (it changes
|
|
26
|
+
nothing), `acceptEdits` for *Edit files*, `auto` for *Auto*, with permission prompts turned down. Never
|
|
27
|
+
`bypassPermissions` or `--dangerously-skip-permissions`; and always `--disallowedTools` for Glow's own CLI
|
|
28
|
+
(`~/.glow/glow`, `npx @winding-labs/glow`, `node …/glow.mjs`) and files (`~/.glow`, the computer's key), which no
|
|
29
|
+
allow rule (the plugin's included) can grant back. That list matches the command's text, so a session already
|
|
30
|
+
allowed arbitrary shell commands (*Auto*, or a broad allow rule in your own or the repo's settings) could spell its
|
|
31
|
+
way around it (`~/.gl?w/glow`); *Look only* and *Edit files* don't allow that, and the CLI also refuses any command
|
|
32
|
+
run from inside a session. One command per call: `glow ignore x run …` is refused, so a skill allowed to run one
|
|
33
|
+
command can't reach another. Stop, a pause in Glow, the 3-hour limit, Ctrl-C or closing the
|
|
34
|
+
terminal ends a session and everything it started: Claude Code's Bash tool and hooks start each command in a process
|
|
35
|
+
group of its own, so the runner signals every group under that Claude Code (SIGTERM, then SIGKILL after 5 s; SIGTERM
|
|
36
|
+
at once if the runner itself exits).
|
|
37
|
+
- **Not take Glow's word for it.** The runner refuses, on your computer: a mode above `--max-mode` (*Look only*
|
|
38
|
+
unless you widen it: `--max-mode edit` or `auto`, also for `/glow:run`); a repo outside `--repo` when you gave it; a folder Claude Code hasn't been trusted in (`claude -p` skips
|
|
39
|
+
that question, and a repo's own `.claude` settings and hooks would apply); a repo with more than one main checkout
|
|
40
|
+
unless you pinned one (`--repo owner/name=path`; its worktrees aren't copies: it runs in the main one); a model or
|
|
41
|
+
effort not on its list (only `--model opus|sonnet|haiku|fable` and `--effort low…max` ever reach `claude`); and a
|
|
42
|
+
session id it has already been given. A command run inside one of
|
|
43
|
+
its sessions can't stop the runner, turn Glow off or change what it sends: besides the denied tools, `glow` refuses
|
|
44
|
+
when its environment has the session's marker (`GLOW_RUN_SESSION`, kept by `~/.glow/glow`), when a session's Claude
|
|
45
|
+
Code is among its parent processes, or when it runs in a session's process group.
|
|
46
|
+
Signed launch tokens (Glow signs each session, the computer checks it with a key it keeps) are a later step: today a
|
|
47
|
+
compromised Glow account or server could start a session within those limits.
|
|
48
|
+
- **Report how a session goes:** running, done, failed or stopped; turns, cost, how many tool calls were turned down;
|
|
49
|
+
and its last step as one line: a command by its program and subcommand only (`$ psql …`, never its arguments), a
|
|
50
|
+
file path relative to the repo, a search pattern or a web host. Keys and tokens are masked before anything is cut,
|
|
51
|
+
the home folder is `~`, and Glow cuts and scrubs it again. Never output, file contents or the transcript.
|
|
16
52
|
|
|
17
53
|
## What the key can do in Glow
|
|
18
54
|
|
|
@@ -25,3 +61,13 @@ stops working when you remove the computer, leave the workspace, or don't use it
|
|
|
25
61
|
|
|
26
62
|
Messages back: only the person who paired the computer can message its sessions, with a fresh second factor each time.
|
|
27
63
|
`npx @winding-labs/glow messages off` turns them off on the computer.
|
|
64
|
+
|
|
65
|
+
Sessions started from Glow: only the person who paired the computer can start one, with a fresh second factor, in a
|
|
66
|
+
repo linked to a product and checked out on the computer, while `glow run` is on there. With the runner on, the key can
|
|
67
|
+
also say which linked repos are checked out, pick up a waiting session (Glow hands the prompt over once and then
|
|
68
|
+
deletes it) and report its status. Glow allows 3 at once per computer, one per repo, and 30 a day, and never more
|
|
69
|
+
than the runner's `--max-mode`; a session not picked up in 10 minutes fails (closed nightly at the latest, prompt
|
|
70
|
+
deleted), and pausing or removing the computer stops its sessions, as does Glow's own kill switch. A queued task never
|
|
71
|
+
runs in *Auto*, and the start sheet says who wrote the task's text. When someone else wrote the task, or it came from a
|
|
72
|
+
capture, the session's reply goes to you only (under the session in the Agents tab), never to a channel. Only you see
|
|
73
|
+
which repos the runner found on your computer.
|
package/bin/glow.mjs
CHANGED
|
@@ -3,20 +3,30 @@
|
|
|
3
3
|
// session in a repo linked to a product posts what it did to that product's channel, and Glow can message it back.
|
|
4
4
|
//
|
|
5
5
|
// npx @winding-labs/glow set up: find the agents, pair (approve in Glow, type the code it shows), add the hooks
|
|
6
|
-
// npx @winding-labs/glow status what's set up, and the repos that report
|
|
6
|
+
// npx @winding-labs/glow status what's set up, what this folder sends, and the repos that report
|
|
7
|
+
// npx @winding-labs/glow ignore [repo] never send from this repo (or a folder / owner/name) on this computer
|
|
8
|
+
// npx @winding-labs/glow unignore [repo] undo that
|
|
7
9
|
// npx @winding-labs/glow off | on stop / start sending from this computer
|
|
8
10
|
// npx @winding-labs/glow messages off stop messages from Glow reaching this computer's agents (on: again)
|
|
9
|
-
// npx @winding-labs/glow
|
|
10
|
-
|
|
11
|
+
// npx @winding-labs/glow recipes off stop sending the request each reply answers (Glow learns recipes from it)
|
|
12
|
+
// npx @winding-labs/glow run [folder…] let Glow start Claude Code sessions here, in the linked repos under these
|
|
13
|
+
// folders (default: this one), while it runs (--background; run stop)
|
|
14
|
+
// npx @winding-labs/glow uninstall remove the hooks and plugins, revoke this computer's key, delete ~/.glow
|
|
15
|
+
//
|
|
16
|
+
// Setup copies this package to ~/.glow (bin, lib, marketplace) with two shims: ~/.glow/hook, which the agents' Stop
|
|
17
|
+
// hooks run, and ~/.glow/glow, this CLI, which the /glow:status, /glow:ignore, /glow:unignore and /glow:run plugin
|
|
18
|
+
// commands run.
|
|
19
|
+
import { execFile, spawn } from 'node:child_process';
|
|
11
20
|
import readline from 'node:readline';
|
|
12
21
|
import fs from 'node:fs';
|
|
13
22
|
import os from 'node:os';
|
|
14
23
|
import path from 'node:path';
|
|
15
24
|
import { fileURLToPath } from 'node:url';
|
|
16
|
-
import { AGENTS, detect, install, installed, shimPath, shq, uninstall } from '../lib/agents.mjs';
|
|
17
|
-
import { api, apiBase, apiOk, computerName, DEFAULT_API, dir, printable, readConfig, repoOf, VERSION, writeConfig } from '../lib/core.mjs';
|
|
25
|
+
import { addPlugin, AGENTS, detect, install, installed, removePlugin, shimPath, shq, uninstall } from '../lib/agents.mjs';
|
|
26
|
+
import { api, apiBase, apiOk, computerName, DEFAULT_API, dir, ignoredOf, printable, readConfig, REPO_RE, repoOf, VERSION, writeConfig } from '../lib/core.mjs';
|
|
27
|
+
import { inRunnerSession, logPath, MODES, runnerState, SESSION_ENV, startRunner } from '../lib/runner.mjs';
|
|
18
28
|
|
|
19
|
-
const
|
|
29
|
+
const PKG = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
30
|
const tty = process.stdout.isTTY;
|
|
21
31
|
const c = (code, s) => (tty ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
22
32
|
const bold = (s) => c(1, s);
|
|
@@ -31,11 +41,23 @@ const fail = (s) => {
|
|
|
31
41
|
const HELP = `Glow ${VERSION}: send what your coding agents do to your Glow channels.
|
|
32
42
|
|
|
33
43
|
npx @winding-labs/glow set up this computer (finds Claude Code and Codex, pairs with Glow, adds hooks)
|
|
34
|
-
npx @winding-labs/glow status what's set up
|
|
44
|
+
npx @winding-labs/glow status what's set up, what this folder sends, which repos report
|
|
45
|
+
npx @winding-labs/glow ignore [repo] never send from this repo on this computer (or a folder, or owner/name)
|
|
46
|
+
npx @winding-labs/glow unignore [repo] let it send again
|
|
35
47
|
npx @winding-labs/glow off | on stop or start sending from this computer
|
|
36
48
|
npx @winding-labs/glow messages off stop messages from Glow reaching your agents here (messages on: again)
|
|
49
|
+
npx @winding-labs/glow recipes off don't send your requests, so Glow learns no recipes from them (recipes on: again)
|
|
50
|
+
npx @winding-labs/glow run [folder…] let Glow start Claude Code sessions here (Agents tab), in the linked repos
|
|
51
|
+
checked out under these folders (default: this one), while it runs
|
|
52
|
+
--background keep it running after this terminal closes (log: ~/.glow/runner.log)
|
|
53
|
+
--max-mode read|edit|auto the most a session may do on its own here, whatever Glow asks (default: read,
|
|
54
|
+
Look only; edit lets it edit files, auto runs Claude Code's auto mode)
|
|
55
|
+
--repo owner/name[=path] only this repo (repeat for more); =path picks its checkout
|
|
56
|
+
npx @winding-labs/glow run stop stop it, and the sessions it runs
|
|
37
57
|
npx @winding-labs/glow uninstall remove the hooks and revoke this computer's key
|
|
38
58
|
|
|
59
|
+
In Claude Code: /glow:status, /glow:ignore, /glow:unignore, /glow:run (in Codex: $glow:status, $glow:ignore, …)
|
|
60
|
+
|
|
39
61
|
Options for setup:
|
|
40
62
|
--claude, --codex only these agents (default: every one found)
|
|
41
63
|
--new pair again, as a new computer
|
|
@@ -44,11 +66,19 @@ Options for setup:
|
|
|
44
66
|
`;
|
|
45
67
|
|
|
46
68
|
function parse(argv) {
|
|
47
|
-
const o = { cmd: 'install', agents: [], api: DEFAULT_API, dev: false, open: true, fresh: false, name: '', messages: null };
|
|
69
|
+
const o = { cmd: 'install', agents: [], api: DEFAULT_API, dev: false, open: true, fresh: false, name: '', messages: null, recipes: null, target: '', roots: [], background: false, runStop: false, maxMode: 'read', allow: new Map() };
|
|
70
|
+
// One command per call: `glow ignore x run …` must not become `run` (a skill allowed to run `glow ignore` can't
|
|
71
|
+
// reach any other command).
|
|
72
|
+
let named = '', asked = '';
|
|
73
|
+
const command = (c) => {
|
|
74
|
+
if (named) fail(`One command at a time (${named}, then ${c}).`);
|
|
75
|
+
named = c;
|
|
76
|
+
o.cmd = c;
|
|
77
|
+
};
|
|
48
78
|
for (let i = 0; i < argv.length; i++) {
|
|
49
79
|
const a = argv[i];
|
|
50
|
-
if (a === '--help' || a === '-h' || a === 'help')
|
|
51
|
-
else if (a === '--version' || a === '-v')
|
|
80
|
+
if (a === '--help' || a === '-h' || a === 'help') asked = 'help';
|
|
81
|
+
else if (a === '--version' || a === '-v') asked = 'version';
|
|
52
82
|
else if (a === '--claude') o.agents.push('claude_code');
|
|
53
83
|
else if (a === '--codex') o.agents.push('codex');
|
|
54
84
|
else if (a === '--new') o.fresh = true;
|
|
@@ -57,11 +87,35 @@ function parse(argv) {
|
|
|
57
87
|
else if (a === '--api') o.api = argv[++i] ?? '';
|
|
58
88
|
else if (a === '--name') o.name = argv[++i] ?? '';
|
|
59
89
|
else if (a === 'messages' && ['on', 'off'].includes(argv[i + 1])) {
|
|
60
|
-
|
|
90
|
+
command('messages');
|
|
61
91
|
o.messages = argv[++i] === 'on';
|
|
62
|
-
} else if (
|
|
92
|
+
} else if (a === 'recipes' && ['on', 'off'].includes(argv[i + 1])) {
|
|
93
|
+
command('recipes');
|
|
94
|
+
o.recipes = argv[++i] === 'on';
|
|
95
|
+
} else if (a === 'ignore' || a === 'unignore') {
|
|
96
|
+
command(a);
|
|
97
|
+
if (argv[i + 1] !== undefined && !argv[i + 1].startsWith('-')) o.target = argv[++i];
|
|
98
|
+
} else if (a === 'run') {
|
|
99
|
+
command('run');
|
|
100
|
+
while (argv[i + 1] !== undefined && !argv[i + 1].startsWith('-')) {
|
|
101
|
+
const x = argv[++i];
|
|
102
|
+
if (x === 'stop' && !o.roots.length) o.runStop = true;
|
|
103
|
+
else o.roots.push(x);
|
|
104
|
+
}
|
|
105
|
+
} else if (a === '--background' && o.cmd === 'run') o.background = true;
|
|
106
|
+
else if (a === '--max-mode' && o.cmd === 'run') {
|
|
107
|
+
o.maxMode = argv[++i] ?? '';
|
|
108
|
+
if (!MODES.includes(o.maxMode)) fail('--max-mode is read, edit or auto.');
|
|
109
|
+
} else if (a === '--repo' && o.cmd === 'run') {
|
|
110
|
+
const [name, at] = String(argv[++i] ?? '').split(/=(.*)/s);
|
|
111
|
+
const repo = name.toLowerCase();
|
|
112
|
+
if (!REPO_RE.test(repo)) fail('--repo is owner/name, or owner/name=path to pick its checkout.');
|
|
113
|
+
o.allow.set(repo, at ? path.resolve(at.replace(/^~(?=$|\/)/, os.homedir())) : null);
|
|
114
|
+
}
|
|
115
|
+
else if (['install', 'setup', 'status', 'off', 'on', 'uninstall'].includes(a)) command(a === 'setup' ? 'install' : a);
|
|
63
116
|
else fail(`Unknown option ${a}. Run npx @winding-labs/glow --help`);
|
|
64
117
|
}
|
|
118
|
+
if (asked) o.cmd = asked; // help and version win, whatever else is there
|
|
65
119
|
if (!apiOk(o.api, o.dev)) fail(`${printable(o.api)} isn’t Glow. The API must be https://app.meetglow.ai (or localhost with --dev).`);
|
|
66
120
|
return o;
|
|
67
121
|
}
|
|
@@ -74,8 +128,13 @@ async function main() {
|
|
|
74
128
|
if (o.cmd === 'help') return console.log(HELP);
|
|
75
129
|
if (o.cmd === 'version') return console.log(VERSION);
|
|
76
130
|
if (o.cmd === 'status') return status();
|
|
131
|
+
// A session Glow started here can't change how Glow reaches this computer (stop the runner, turn sending off, …).
|
|
132
|
+
if (inRunnerSession()) fail('Not from a session Glow started: run it yourself, in a terminal.');
|
|
77
133
|
if (o.cmd === 'off' || o.cmd === 'on') return toggle(o.cmd === 'off');
|
|
78
134
|
if (o.cmd === 'messages') return messages(o.messages);
|
|
135
|
+
if (o.cmd === 'recipes') return recipes(o.recipes);
|
|
136
|
+
if (o.cmd === 'ignore' || o.cmd === 'unignore') return ignore(o.target, o.cmd === 'ignore');
|
|
137
|
+
if (o.cmd === 'run') return run(o);
|
|
79
138
|
if (o.cmd === 'uninstall') return remove();
|
|
80
139
|
return setup(o);
|
|
81
140
|
}
|
|
@@ -118,7 +177,7 @@ async function setup(o) {
|
|
|
118
177
|
writeConfig({ ...cfg, agents: [...new Set([...(cfg.agents ?? []), ...agents])], ...(info?.repos ? { repos: info.repos, repos_at: Date.now() } : {}), version: VERSION });
|
|
119
178
|
}
|
|
120
179
|
|
|
121
|
-
|
|
180
|
+
copyFiles();
|
|
122
181
|
for (const a of agents) {
|
|
123
182
|
try {
|
|
124
183
|
const file = install(a);
|
|
@@ -126,18 +185,29 @@ async function setup(o) {
|
|
|
126
185
|
} catch (e) {
|
|
127
186
|
warn(`${AGENTS[a].label}: couldn’t update ${tilde(path.join(os.homedir(), ...AGENTS[a].file))} (${e.message}). Fix it, then run this again.`);
|
|
128
187
|
}
|
|
188
|
+
try {
|
|
189
|
+
addPlugin(a);
|
|
190
|
+
const x = a === 'codex' ? '$' : '/';
|
|
191
|
+
ok(`${AGENTS[a].label}: ${x}glow:status, ${x}glow:ignore, ${x}glow:unignore and ${x}glow:run added`);
|
|
192
|
+
} catch (e) {
|
|
193
|
+
warn(`${AGENTS[a].label}: couldn’t add the /glow commands (${printable(e.message, 160)}). The hook works without them.`);
|
|
194
|
+
}
|
|
129
195
|
}
|
|
130
196
|
|
|
131
|
-
const
|
|
197
|
+
const ignored = ignoredOf(readConfig());
|
|
198
|
+
const repos = (info?.repos ?? []).filter((r) => !ignored.includes(r)).map((r) => printable(r, 160));
|
|
132
199
|
console.log('');
|
|
133
200
|
console.log(repos.length ? `Repos that report to Glow: ${repos.join(', ')}` : 'No repos are linked to a product yet.');
|
|
201
|
+
if (ignored.length) console.log(`Ignored on this computer: ${ignored.join(', ')}`);
|
|
134
202
|
const here = repoOf(process.cwd());
|
|
135
|
-
if (here && !repos.includes(here)) console.log(`This repo (${here}) isn’t linked. Link it to a product: ${
|
|
203
|
+
if (here && !repos.includes(here) && !ignored.includes(here)) console.log(`This repo (${here}) isn’t linked. Link it to a product: ${linkUrl(cfg, here)}`);
|
|
136
204
|
console.log('');
|
|
137
|
-
if (agents.includes('claude_code')) console.log(`${bold('Claude Code')}: new sessions in those repos post to Glow when they finish a reply. Restart open sessions to pick it up.`);
|
|
205
|
+
if (agents.includes('claude_code')) console.log(`${bold('Claude Code')}: new sessions in those repos post to Glow when they finish a reply. Restart open sessions to pick it up (and the /glow commands).`);
|
|
138
206
|
if (agents.includes('codex')) console.log(`${bold('Codex')}: open Codex and run ${bold('/hooks')} once to trust the Glow hook (Codex asks before running any new hook).`);
|
|
139
207
|
console.log(`Messages you send a session from its post in Glow reach that agent${readConfig()?.messages === false ? ' (off here: npx @winding-labs/glow messages on)' : ' (npx @winding-labs/glow messages off to stop that)'}.`);
|
|
140
|
-
console.log(
|
|
208
|
+
if (agents.includes('claude_code')) console.log(`To start Claude Code sessions here from Glow (Agents tab): ${bold('npx @winding-labs/glow run')} in your code folder.`);
|
|
209
|
+
console.log(dim('\nOnly sessions in linked repos send anything, and only to Glow. A repo you never want sent: /glow:ignore in it.'));
|
|
210
|
+
console.log(dim('Off: npx @winding-labs/glow off · Remove: npx @winding-labs/glow uninstall'));
|
|
141
211
|
}
|
|
142
212
|
|
|
143
213
|
/** The device flow: show a code, open Glow to approve it, then type the confirmation Glow shows to get the key. */
|
|
@@ -213,22 +283,38 @@ async function pair(o, agents) {
|
|
|
213
283
|
}
|
|
214
284
|
|
|
215
285
|
|
|
216
|
-
/**
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
286
|
+
/**
|
|
287
|
+
* This package's code to ~/.glow (bin, lib, marketplace) and the two shims: ~/.glow/hook (the agents' Stop hooks) and
|
|
288
|
+
* ~/.glow/glow (this CLI, for the /glow commands). Both run the Node that ran this (or the one on PATH) with an empty
|
|
289
|
+
* environment but HOME and PATH: no NODE_OPTIONS, proxy, TLS or GIT_* variable from the agent's settings can change
|
|
290
|
+
* where they send or what they run. (GLOW_POLL_MS only speeds up the tests.)
|
|
291
|
+
*/
|
|
292
|
+
function copyFiles() {
|
|
293
|
+
const home = dir();
|
|
294
|
+
fs.mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
295
|
+
fs.chmodSync(home, 0o700);
|
|
296
|
+
// Copied over in place (never removed first), so a hook that starts meanwhile still finds its files.
|
|
297
|
+
if (fs.realpathSync(PKG) !== fs.realpathSync(home)) {
|
|
298
|
+
for (const d of ['bin', 'lib', 'marketplace']) fs.cpSync(path.join(PKG, d), path.join(home, d), { recursive: true, force: true });
|
|
299
|
+
}
|
|
300
|
+
// 0.1.0–0.1.1 kept the hook at ~/.glow/hook.mjs and ~/.glow/core.mjs.
|
|
301
|
+
for (const f of ['core.mjs', 'hook.mjs']) fs.rmSync(path.join(home, f), { force: true });
|
|
302
|
+
const shim = (what, script, extra = '') => `#!/bin/sh
|
|
303
|
+
# ${what} (npx @winding-labs/glow ${VERSION})
|
|
226
304
|
NODE=${shq(process.execPath)}
|
|
227
|
-
[ -x "$NODE" ] || NODE=$(command -v node) || exit 0
|
|
228
|
-
exec env -i HOME="$HOME" PATH="$PATH"
|
|
305
|
+
[ -x "$NODE" ] || NODE=$(command -v node) || { echo "Glow needs Node 18 or newer: https://nodejs.org" >&2; exit 0; }
|
|
306
|
+
exec env -i HOME="$HOME" PATH="$PATH" ${extra}"$NODE" ${shq(path.join(home, script))} "$@"
|
|
229
307
|
`;
|
|
230
|
-
|
|
231
|
-
|
|
308
|
+
writeShim(shimPath(), shim('Glow agent hook: sends coding agent replies in linked repos to Glow', path.join('lib', 'hook.mjs'), '${GLOW_POLL_MS:+GLOW_POLL_MS="$GLOW_POLL_MS"} '));
|
|
309
|
+
// The session marker goes through (env -i drops the rest): glow refuses to change anything from a runner's session.
|
|
310
|
+
writeShim(cliPath(), shim('Glow: status, ignore, unignore, run and the rest of npx @winding-labs/glow, for the /glow commands', path.join('bin', 'glow.mjs'), `\${${SESSION_ENV}:+${SESSION_ENV}="$${SESSION_ENV}"} `));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const cliPath = () => path.join(dir(), 'glow');
|
|
314
|
+
|
|
315
|
+
function writeShim(file, text) {
|
|
316
|
+
fs.writeFileSync(file, text, { mode: 0o700 });
|
|
317
|
+
fs.chmodSync(file, 0o700);
|
|
232
318
|
}
|
|
233
319
|
|
|
234
320
|
function openUrl(url) {
|
|
@@ -241,20 +327,80 @@ function openUrl(url) {
|
|
|
241
327
|
}
|
|
242
328
|
|
|
243
329
|
const tilde = (p) => (p.startsWith(os.homedir()) ? `~${p.slice(os.homedir().length)}` : p);
|
|
330
|
+
const linkUrl = (cfg, repo) => `${apiBase(cfg.api)}/app/pair?repo=${encodeURIComponent(repo)}`;
|
|
244
331
|
|
|
245
332
|
// ---------- status, off/on, uninstall ----------
|
|
246
333
|
|
|
334
|
+
/** What's set up here, what this folder sends (and why not), and the repos that report or are ignored. */
|
|
247
335
|
async function status() {
|
|
248
336
|
const cfg = readConfig();
|
|
249
|
-
if (!cfg) return console.log('
|
|
337
|
+
if (!cfg) return console.log('Glow isn’t set up on this computer, so nothing is sent from it. Set it up: npx @winding-labs/glow');
|
|
250
338
|
const res = await api(cfg, 'GET', '/api/v1/agent/config', undefined, 10_000);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
339
|
+
const ok200 = res.status === 200 && Array.isArray(res.json?.repos);
|
|
340
|
+
try {
|
|
341
|
+
if (ok200) writeConfig({ ...(readConfig() ?? cfg), repos: res.json.repos.filter((r) => typeof r === 'string').slice(0, 200), repos_at: Date.now() });
|
|
342
|
+
} catch {
|
|
343
|
+
// a sandbox that can't write ~/.glow: the hook refreshes the list itself
|
|
344
|
+
}
|
|
345
|
+
const linkedRepos = (ok200 ? res.json.repos : Array.isArray(cfg.repos) ? cfg.repos : []).filter((r) => typeof r === 'string').map((r) => printable(r, 160));
|
|
346
|
+
const ignored = ignoredOf(cfg);
|
|
347
|
+
const key = res.status === 200 ? 'key works' : res.status === 401 ? 'key revoked: run npx @winding-labs/glow to pair again' : res.status === 403 ? 'paused in Glow (Connections › Coding agents)' : `Glow unreachable (${res.status || 'offline'})`;
|
|
348
|
+
|
|
349
|
+
const here = repoOf(process.cwd());
|
|
350
|
+
let folder;
|
|
351
|
+
if (!here) folder = 'not a GitHub repo: sends nothing';
|
|
352
|
+
else if (ignored.includes(here)) folder = `${here}: ignored here, sends nothing (undo: /glow:unignore)`;
|
|
353
|
+
else if (!linkedRepos.includes(here)) folder = `${here}: not linked to a product, sends nothing. Link it: ${linkUrl(cfg, here)}`;
|
|
354
|
+
else if (cfg.off) folder = `${here}: linked, but sending is off on this computer`;
|
|
355
|
+
else if (res.status === 401 || res.status === 403) folder = `${here}: linked, but this computer can’t send (see Computer)`;
|
|
356
|
+
else folder = `${here}: reports to Glow`;
|
|
357
|
+
|
|
358
|
+
const agents = Object.keys(AGENTS).map((a) => `${AGENTS[a].label} ${installed(a) ? 'hooked' : 'not hooked'}`).join(' · ');
|
|
359
|
+
const reporting = linkedRepos.filter((r) => !ignored.includes(r));
|
|
360
|
+
const rs = runnerState();
|
|
361
|
+
const rows = [
|
|
362
|
+
['This folder', folder],
|
|
363
|
+
['Computer', `${printable(cfg.computer?.name) || '?'} · workspace ${printable(res.json?.workspace?.name ?? cfg.workspace?.name) || '?'} · ${key}`],
|
|
364
|
+
['Sending', `${cfg.off ? 'off (npx @winding-labs/glow on)' : 'on'} · messages from Glow ${cfg.messages === false ? 'off' : 'on'} · requests for recipes ${cfg.recipes === false ? 'off' : 'on'}`],
|
|
365
|
+
['Agents', agents],
|
|
366
|
+
['Reporting', reporting.length ? reporting.join(', ') : 'no repo yet'],
|
|
367
|
+
['Ignored here', ignored.length ? ignored.join(', ') : 'none'],
|
|
368
|
+
['Runner', rs
|
|
369
|
+
? `on since ${new Date(rs.started_at).toLocaleString()} (pid ${rs.pid}) in ${(rs.roots ?? []).map((r) => printable(r, 160)).join(', ')}: Glow can start Claude Code sessions here · stop: npx @winding-labs/glow run stop`
|
|
370
|
+
: 'off: Glow can’t start sessions here (to let it: npx @winding-labs/glow run, in your code folder)'],
|
|
371
|
+
];
|
|
372
|
+
console.log(`${bold('Glow')} ${dim(VERSION)} · ${apiBase(cfg.api)}\n`);
|
|
373
|
+
for (const [k, v] of rows) console.log(`${dim(k.padEnd(14))}${v}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** The repo a folder is in (default: this one), or an owner/name typed out (or its GitHub URL). */
|
|
377
|
+
function repoArg(target) {
|
|
378
|
+
if (!target) return { repo: repoOf(process.cwd()), where: 'This folder' };
|
|
379
|
+
const p = path.resolve(target.replace(/^~(?=$|\/)/, os.homedir()));
|
|
380
|
+
if (fs.existsSync(p)) return { repo: repoOf(p), where: tilde(p) };
|
|
381
|
+
const r = target.trim().toLowerCase().replace(/^(?:https:\/\/)?github\.com\//, '').replace(/(?:\.git)?\/?$/, '');
|
|
382
|
+
return { repo: REPO_RE.test(r) ? r : null, where: printable(target, 160) };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Ignore (or stop ignoring) a repo on this computer. Local only: the hook checks it before sending anything. */
|
|
386
|
+
function ignore(target, on) {
|
|
387
|
+
const { repo, where } = repoArg(target);
|
|
388
|
+
if (!repo) return console.log(`${where} isn’t a GitHub repo, so Glow sends nothing from it.`);
|
|
389
|
+
const cfg = readConfig();
|
|
390
|
+
if (!cfg) return console.log(`Glow isn’t set up on this computer, so nothing is sent from ${repo} (or any repo).`);
|
|
391
|
+
const list = ignoredOf(cfg);
|
|
392
|
+
const linked = Array.isArray(cfg.repos) && cfg.repos.includes(repo);
|
|
393
|
+
if (on) {
|
|
394
|
+
if (list.includes(repo)) return ok(`${repo} is already ignored here: nothing is sent from it.`);
|
|
395
|
+
writeConfig({ ...cfg, ignored: [...list, repo].sort() });
|
|
396
|
+
ok(`Ignored ${repo}: Claude Code and Codex sessions in it send nothing to Glow from this computer.`);
|
|
397
|
+
console.log(dim(`${linked ? 'It’s linked to a product in Glow; other computers still report from it. ' : ''}Undo: /glow:unignore ($glow:unignore in Codex, or npx @winding-labs/glow unignore${target ? ` ${repo}` : ''})`));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (!list.includes(repo)) return console.log(`${repo} isn’t ignored here. ${linked ? 'Sessions in it report to Glow.' : 'It isn’t linked to a product, so it sends nothing.'}`);
|
|
401
|
+
writeConfig({ ...cfg, ignored: list.filter((r) => r !== repo) });
|
|
402
|
+
ok(`${repo} isn’t ignored anymore.`);
|
|
403
|
+
console.log(linked ? 'Sessions in it report to Glow again.' : `It isn’t linked to a product, so it still sends nothing. Link it: ${linkUrl(cfg, repo)}`);
|
|
258
404
|
}
|
|
259
405
|
|
|
260
406
|
function toggle(off) {
|
|
@@ -271,8 +417,25 @@ function messages(on) {
|
|
|
271
417
|
ok(on ? 'Messages on: what you send a session from Glow reaches its agent' : 'Messages off: Glow holds nothing for this computer’s agents (npx @winding-labs/glow messages on to undo)');
|
|
272
418
|
}
|
|
273
419
|
|
|
420
|
+
function recipes(on) {
|
|
421
|
+
const cfg = readConfig();
|
|
422
|
+
if (!cfg) fail('Not set up. Run npx @winding-labs/glow');
|
|
423
|
+
writeConfig({ ...cfg, recipes: on });
|
|
424
|
+
ok(on ? 'Recipes on: each report carries the request it answers, and Glow learns the recipes you run' : 'Recipes off: reports carry the agent’s reply only (npx @winding-labs/glow recipes on to undo)');
|
|
425
|
+
}
|
|
426
|
+
|
|
274
427
|
async function remove() {
|
|
275
428
|
const cfg = readConfig();
|
|
429
|
+
const rs = runnerState();
|
|
430
|
+
if (rs) {
|
|
431
|
+
try {
|
|
432
|
+
process.kill(rs.pid, 'SIGTERM');
|
|
433
|
+
for (let n = 0; n < 100 && runnerState(); n++) await sleep(100);
|
|
434
|
+
ok('Runner stopped');
|
|
435
|
+
} catch {
|
|
436
|
+
warn(`Couldn’t stop the runner (pid ${rs.pid}). Stop it yourself: kill ${rs.pid}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
276
439
|
for (const a of Object.keys(AGENTS)) {
|
|
277
440
|
try {
|
|
278
441
|
const file = uninstall(a);
|
|
@@ -280,6 +443,7 @@ async function remove() {
|
|
|
280
443
|
} catch (e) {
|
|
281
444
|
warn(`${AGENTS[a].label}: couldn’t update its settings (${e.message}); remove the Glow hook by hand.`);
|
|
282
445
|
}
|
|
446
|
+
if (detect().includes(a) && removePlugin(a)) ok(`${AGENTS[a].label}: /glow commands removed`);
|
|
283
447
|
}
|
|
284
448
|
if (cfg) {
|
|
285
449
|
const res = await api(cfg, 'DELETE', '/api/v1/agent/computer', undefined, 10_000);
|
|
@@ -290,4 +454,81 @@ async function remove() {
|
|
|
290
454
|
ok(`Deleted ${tilde(dir())}`);
|
|
291
455
|
}
|
|
292
456
|
|
|
457
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
458
|
+
|
|
459
|
+
// ---------- run: sessions Glow starts here ----------
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* `glow run [folder…]`: Glow can start Claude Code sessions here while this runs (lib/runner.mjs). In the foreground
|
|
463
|
+
* (Ctrl-C stops it and its sessions), or with --background as its own process logging to ~/.glow/runner.log. One per
|
|
464
|
+
* computer. `glow run stop` ends it.
|
|
465
|
+
*/
|
|
466
|
+
async function run(o) {
|
|
467
|
+
const cfg = readConfig();
|
|
468
|
+
if (!cfg) fail('Glow isn’t set up on this computer. Set it up first: npx @winding-labs/glow');
|
|
469
|
+
const on = runnerState();
|
|
470
|
+
if (o.runStop) {
|
|
471
|
+
if (!on) return console.log('The runner isn’t on here, so Glow can’t start sessions on this computer.');
|
|
472
|
+
process.kill(on.pid, 'SIGTERM');
|
|
473
|
+
for (let n = 0; n < 120 && runnerState(); n++) await sleep(100);
|
|
474
|
+
return runnerState() ? warn(`Asked the runner (pid ${on.pid}) to stop; it’s still finishing.`) : ok('Runner stopped: Glow can’t start sessions here until you run it again.');
|
|
475
|
+
}
|
|
476
|
+
if (cfg.off) fail('Sending is off on this computer. Turn it on first: npx @winding-labs/glow on');
|
|
477
|
+
if (on) fail(`The runner is already on here (pid ${on.pid}, in ${(on.roots ?? []).map((r) => printable(r, 160)).join(', ')}). Stop it first: npx @winding-labs/glow run stop`);
|
|
478
|
+
const roots = (o.roots.length ? o.roots : [process.cwd()]).map((r) => path.resolve(r.replace(/^~(?=$|\/)/, os.homedir())));
|
|
479
|
+
for (const r of roots) if (!fs.existsSync(r) || !fs.statSync(r).isDirectory()) fail(`${printable(r, 160)} isn’t a folder.`);
|
|
480
|
+
// From 0.1.4 the hook leaves the sessions the runner starts to the runner (one report each): bring an older copy up.
|
|
481
|
+
let hookVersion;
|
|
482
|
+
try {
|
|
483
|
+
hookVersion = /VERSION = '([\d.]+)'/.exec(fs.readFileSync(path.join(dir(), 'lib', 'core.mjs'), 'utf8'))?.[1];
|
|
484
|
+
} catch {
|
|
485
|
+
hookVersion = undefined;
|
|
486
|
+
}
|
|
487
|
+
const older = (a, b) => {
|
|
488
|
+
const [x, y] = [a, b].map((v) => v.split('.').map(Number));
|
|
489
|
+
for (let i = 0; i < 3; i++) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) < (y[i] ?? 0);
|
|
490
|
+
return false;
|
|
491
|
+
};
|
|
492
|
+
let shimNow = '';
|
|
493
|
+
try {
|
|
494
|
+
shimNow = fs.readFileSync(cliPath(), 'utf8');
|
|
495
|
+
} catch {
|
|
496
|
+
shimNow = '';
|
|
497
|
+
}
|
|
498
|
+
// Older, or a ~/.glow/glow that drops the session marker (an early 0.1.4): bring it up.
|
|
499
|
+
if (hookVersion && /^\d+\.\d+\.\d+$/.test(VERSION) && (older(hookVersion, VERSION) || (shimNow && !shimNow.includes(SESSION_ENV)))) {
|
|
500
|
+
copyFiles();
|
|
501
|
+
console.log(dim(older(hookVersion, VERSION) ? `Updated Glow’s hook on this computer from ${hookVersion} to ${VERSION}.` : 'Updated Glow’s files on this computer.'));
|
|
502
|
+
}
|
|
503
|
+
if (o.background) {
|
|
504
|
+
const log = logPath();
|
|
505
|
+
fs.mkdirSync(dir(), { recursive: true, mode: 0o700 });
|
|
506
|
+
try {
|
|
507
|
+
if (fs.statSync(log).size > 1_000_000) fs.rmSync(log, { force: true });
|
|
508
|
+
} catch {
|
|
509
|
+
// no log yet
|
|
510
|
+
}
|
|
511
|
+
const fd = fs.openSync(log, 'a', 0o600);
|
|
512
|
+
fs.chmodSync(log, 0o600);
|
|
513
|
+
const flags = ['--max-mode', o.maxMode, ...[...o.allow].flatMap(([r, at]) => ['--repo', at ? `${r}=${at}` : r])];
|
|
514
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), 'run', ...roots, ...flags], { detached: true, stdio: ['ignore', fd, fd], env: process.env, cwd: roots[0] });
|
|
515
|
+
child.unref();
|
|
516
|
+
for (let n = 0; n < 80 && runnerState()?.pid !== child.pid; n++) await sleep(100);
|
|
517
|
+
if (runnerState()?.pid !== child.pid) fail(`The runner didn’t start. See ${tilde(log)}`);
|
|
518
|
+
ok(`Runner on in the background (pid ${child.pid}): Glow can start Claude Code sessions in the linked repos under ${roots.map(tilde).join(', ')}.`);
|
|
519
|
+
console.log(dim(`Log: ${tilde(log)} · Stop: npx @winding-labs/glow run stop`));
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
for (const [r, at] of o.allow) if (at && (!fs.existsSync(at) || !fs.statSync(at).isDirectory())) fail(`${printable(at, 160)} (for ${r}) isn’t a folder.`);
|
|
523
|
+
const r = startRunner({ roots, say: (line) => console.log(line), maxMode: o.maxMode, allow: o.allow });
|
|
524
|
+
const quit = () => {
|
|
525
|
+
console.log(dim('\nStopping the runner and its sessions…'));
|
|
526
|
+
void r.stop();
|
|
527
|
+
};
|
|
528
|
+
process.once('SIGINT', quit);
|
|
529
|
+
process.once('SIGTERM', quit);
|
|
530
|
+
process.once('SIGHUP', quit); // the terminal closed
|
|
531
|
+
process.exit(await r.done);
|
|
532
|
+
}
|
|
533
|
+
|
|
293
534
|
main().catch((e) => fail(e?.message ?? String(e)));
|