flowviant 0.100.0 → 0.101.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/README.md +4 -0
- package/bin/cli.mjs +21 -1
- package/bin/lib/login.mjs +7 -3
- package/bin/lib/tty.mjs +2 -2
- package/bin/lib/uninstall.mjs +287 -0
- package/bin/lib/update.mjs +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,6 +52,10 @@ flowviant login
|
|
|
52
52
|
|
|
53
53
|
The binary and global npm installs update when idle. The binary fetches the release manifest and verifies the new executable before replacing itself. The global install refreshes from npm before restarting. `flowviant update` checks on demand. `FLOWVIANT_NO_UPDATE=1` makes updates manual.
|
|
54
54
|
|
|
55
|
+
## Uninstall
|
|
56
|
+
|
|
57
|
+
`flowviant uninstall` stops local daemons and removes installed copies. It keeps logins and local state in `~/.flowviant`, so reinstalling resumes them. Use `flowviant uninstall --purge` to disconnect this box from stored projects and delete that local data too. Add `--yes` to skip confirmation.
|
|
58
|
+
|
|
55
59
|
## Machines on this box
|
|
56
60
|
|
|
57
61
|
One box can serve several projects — one daemon per repository directory, each connected with its own `flowviant login` run inside that repo. `flowviant machines` lists every project connected on this box with the id and date it was connected, and under each one every computer that has polled it (the app's Home lists the same across your whole account). Two projects bound to one repository are named out loud at the foot of the listing, because that is the one shape that reads as "the same project connected twice" and is not — and a directory serves one project, so `flowviant` there refuses to start until one of them goes.
|
package/bin/cli.mjs
CHANGED
|
@@ -70,10 +70,15 @@
|
|
|
70
70
|
* env (the box keypair, the uplink scrubber, the env-comparison scan) + vault
|
|
71
71
|
* (the knowledge wiki, not secrets), resources, deploy, shot.
|
|
72
72
|
*/
|
|
73
|
+
// The tray app reads `login --json` through a pipe (wsl.exe). A line written
|
|
74
|
+
// just before process.exit() can be dropped there, and then all the tray knows
|
|
75
|
+
// is that the login ended; wait for stdout to drain before exiting.
|
|
76
|
+
const flushStdout = () => new Promise((resolve) => process.stdout.write('', resolve));
|
|
77
|
+
|
|
73
78
|
// Resolve --dir before config reads the project bound to cwd. A tray process
|
|
74
79
|
// starts outside the checkout; its explicit folder is the daemon's checkout.
|
|
75
80
|
const dirAt = process.argv.indexOf('--dir');
|
|
76
|
-
const noCheckoutCommand = new Set(['--version', '-v', 'version', 'status', 'stop', 'machines', 'projects', 'update', 'shot', 'clean', 'gh-auth', 'mcp']);
|
|
81
|
+
const noCheckoutCommand = new Set(['--version', '-v', 'version', 'status', 'stop', 'uninstall', 'machines', 'projects', 'update', 'shot', 'clean', 'gh-auth', 'mcp']);
|
|
77
82
|
if (dirAt >= 0 && !noCheckoutCommand.has(process.argv[2])) {
|
|
78
83
|
const dir = process.argv[dirAt + 1];
|
|
79
84
|
try {
|
|
@@ -83,6 +88,7 @@ if (dirAt >= 0 && !noCheckoutCommand.has(process.argv[2])) {
|
|
|
83
88
|
if (process.argv.includes('--json') && process.argv[2] === 'login')
|
|
84
89
|
process.stdout.write(`${JSON.stringify({ event: 'error', message: `Cannot use directory ${dir ?? ''}: ${e.message}` })}\n`);
|
|
85
90
|
else console.error(`error: cannot use directory ${dir ?? ''}: ${e.message}`);
|
|
91
|
+
await flushStdout();
|
|
86
92
|
process.exit(1);
|
|
87
93
|
}
|
|
88
94
|
}
|
|
@@ -122,6 +128,7 @@ if (process.argv[2] === 'login') {
|
|
|
122
128
|
// A login the person CANCELLED at the second-project question saved nothing,
|
|
123
129
|
// so there is no credential for the child to serve — starting it would end
|
|
124
130
|
// in "no credential found" over a choice they just made on purpose.
|
|
131
|
+
if (json) await flushStdout();
|
|
125
132
|
if (!login?.saved) process.exit(1); // refused: the directory already serves another project
|
|
126
133
|
if (noStart) process.exit(0);
|
|
127
134
|
// Re-exec as a plain `flowviant` rather than falling through. config.mjs reads
|
|
@@ -243,6 +250,19 @@ if (process.argv[2] === 'stop') {
|
|
|
243
250
|
process.exit(failed > 0 ? 1 : 0);
|
|
244
251
|
}
|
|
245
252
|
|
|
253
|
+
if (process.argv[2] === 'uninstall') {
|
|
254
|
+
const { runUninstall } = await import('./lib/uninstall.mjs');
|
|
255
|
+
const { askWithTimeout } = await import('./lib/tty.mjs');
|
|
256
|
+
const result = await runUninstall({
|
|
257
|
+
others: process.argv.includes('--others'),
|
|
258
|
+
purge: process.argv.includes('--purge'),
|
|
259
|
+
yes: process.argv.includes('--yes'),
|
|
260
|
+
json: process.argv.includes('--json'),
|
|
261
|
+
prompt: (query) => askWithTimeout(query, undefined, process.stderr),
|
|
262
|
+
});
|
|
263
|
+
process.exit(result.ok ? 0 : 1);
|
|
264
|
+
}
|
|
265
|
+
|
|
246
266
|
// `flowviant projects` — every project this box has a credential for, which
|
|
247
267
|
// repo each is bound to, and which the legacy mirror points at. Needs no
|
|
248
268
|
// network: it reads the store, which is the exact thing a confused person is
|
package/bin/lib/login.mjs
CHANGED
|
@@ -51,10 +51,14 @@ export async function runLogin({ thenStart = false, json = false, dir = process.
|
|
|
51
51
|
// section deleted 2026-08-17; connecting a machine is offered on the surface
|
|
52
52
|
// you are on when it matters, and for a new operator that is the Workbench —
|
|
53
53
|
// the project's empty state says so before it can show you any sessions.
|
|
54
|
-
|
|
54
|
+
// `/connect?code=` (2026-09-25) takes the code, asks which project, and
|
|
55
|
+
// approves: the code no longer has to be carried by hand into a project's
|
|
56
|
+
// Connect a machine panel, which is where people got lost.
|
|
57
|
+
const approveUrl = `${APP_URL}/connect?code=${encodeURIComponent(pretty)}`;
|
|
58
|
+
if (json) event({ event: 'open_url', url: approveUrl, code: pretty });
|
|
55
59
|
else {
|
|
56
|
-
console.log(`
|
|
57
|
-
console.log(`
|
|
60
|
+
console.log(` Open ${c.cyan(approveUrl)}`);
|
|
61
|
+
console.log(` and approve this computer for your project. Check the code there is ${c.bold(c.green(pretty))}.\n`);
|
|
58
62
|
info('waiting for you to approve…');
|
|
59
63
|
}
|
|
60
64
|
|
package/bin/lib/tty.mjs
CHANGED
|
@@ -112,7 +112,7 @@ export function boundedTimer(timeoutMs, fire) {
|
|
|
112
112
|
* answer everywhere (the binding confirm serves unbound; the project picker
|
|
113
113
|
* refuses, exactly as it does headless).
|
|
114
114
|
*/
|
|
115
|
-
export async function askWithTimeout(query, timeoutMs) {
|
|
115
|
+
export async function askWithTimeout(query, timeoutMs, output = process.stdout) {
|
|
116
116
|
const noop = () => {};
|
|
117
117
|
// Replacing the DEFAULT disposition is the whole point — an empty handler is
|
|
118
118
|
// enough, and it is what keeps the timer below able to run at all.
|
|
@@ -120,7 +120,7 @@ export async function askWithTimeout(query, timeoutMs) {
|
|
|
120
120
|
process.on('SIGTTOU', noop);
|
|
121
121
|
const rl = (await import('node:readline/promises')).createInterface({
|
|
122
122
|
input: process.stdin,
|
|
123
|
-
output
|
|
123
|
+
output,
|
|
124
124
|
});
|
|
125
125
|
const ac = new AbortController();
|
|
126
126
|
const timer = boundedTimer(timeoutMs, () => ac.abort());
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import * as nodeFs from 'node:fs';
|
|
2
|
+
import { execFile as nodeExecFile } from 'node:child_process';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { runningCompiledBinary } from './update.mjs';
|
|
7
|
+
import { VERSION } from './config.mjs';
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(nodeExecFile);
|
|
10
|
+
const inside = (path, dir) => path === dir || path.startsWith(`${dir}${sep}`);
|
|
11
|
+
const errorWords = (error) => String(error?.stderr || error?.message || error).trim();
|
|
12
|
+
|
|
13
|
+
function defaults(options = {}) {
|
|
14
|
+
return {
|
|
15
|
+
fs: nodeFs,
|
|
16
|
+
home: homedir(),
|
|
17
|
+
path: process.env.PATH || '',
|
|
18
|
+
execFile: (file, args, opts) => execFileAsync(file, args, opts),
|
|
19
|
+
pidLive: (pid) => { try { process.kill(pid, 0); return true; } catch { return false; } },
|
|
20
|
+
procReader: (pid, file) => file === 'exe' || file === 'cwd'
|
|
21
|
+
? nodeFs.readlinkSync(`/proc/${pid}/${file}`) : nodeFs.readFileSync(`/proc/${pid}/${file}`),
|
|
22
|
+
procAvailable: nodeFs.existsSync('/proc/self/cmdline'),
|
|
23
|
+
compiled: runningCompiledBinary(),
|
|
24
|
+
execPath: process.execPath,
|
|
25
|
+
argvPath: process.argv[1],
|
|
26
|
+
stopAllDaemons: null,
|
|
27
|
+
projects: null,
|
|
28
|
+
disconnect: null,
|
|
29
|
+
prompt: null,
|
|
30
|
+
tty: Boolean(process.stdin.isTTY),
|
|
31
|
+
log: (line) => console.error(line),
|
|
32
|
+
stdout: (line) => process.stdout.write(`${line}\n`),
|
|
33
|
+
...options,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function real(fs, path) { try { return fs.realpathSync(path); } catch { return resolve(path); } }
|
|
38
|
+
function readVersion(fs, path) {
|
|
39
|
+
try {
|
|
40
|
+
const value = JSON.parse(fs.readFileSync(path, 'utf8')).version;
|
|
41
|
+
return typeof value === 'string' ? value : undefined;
|
|
42
|
+
} catch { return undefined; }
|
|
43
|
+
}
|
|
44
|
+
function files(fs, dir) { try { return fs.readdirSync(dir); } catch { return []; } }
|
|
45
|
+
function regular(fs, path) { try { return fs.statSync(path).isFile(); } catch { return false; } }
|
|
46
|
+
function executable(fs, path) { try { const stat = fs.statSync(path); return stat.isFile() && Boolean(stat.mode & 0o111); } catch { return false; } }
|
|
47
|
+
// A compiled flowviant is an ELF or Mach-O file. A shell wrapper someone named
|
|
48
|
+
// `flowviant` is not a copy of ours and is never removed.
|
|
49
|
+
function compiledExecutable(fs, path) {
|
|
50
|
+
try {
|
|
51
|
+
const fd = fs.openSync(path, 'r');
|
|
52
|
+
const head = Buffer.alloc(4);
|
|
53
|
+
try { fs.readSync(fd, head, 0, 4, 0); } finally { fs.closeSync(fd); }
|
|
54
|
+
const magic = head.readUInt32BE(0);
|
|
55
|
+
return magic === 0x7f454c46 || [0xfeedfacf, 0xcffaedfe, 0xfeedface, 0xcefaedfe, 0xcafebabe].includes(magic);
|
|
56
|
+
} catch { return false; }
|
|
57
|
+
}
|
|
58
|
+
function exactBlock(dir, rc) {
|
|
59
|
+
const line = basename(rc) === 'flowviant.fish'
|
|
60
|
+
? `fish_add_path "${dir}"` : `export PATH="${dir}:$PATH"`;
|
|
61
|
+
return `\n# flowviant\n${line}\n`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function liveDaemons(o) {
|
|
65
|
+
const found = [];
|
|
66
|
+
for (const name of files(o.fs, join(o.home, '.flowviant'))) {
|
|
67
|
+
if (!/^daemon-[0-9a-f]{12}\.lock$/.test(name)) continue;
|
|
68
|
+
try {
|
|
69
|
+
const pid = JSON.parse(o.fs.readFileSync(join(o.home, '.flowviant', name), 'utf8')).pid;
|
|
70
|
+
if (!Number.isInteger(pid) || pid <= 0 || !o.pidLive(pid)) continue;
|
|
71
|
+
let paths = [];
|
|
72
|
+
if (o.procAvailable) {
|
|
73
|
+
try {
|
|
74
|
+
const exe = o.procReader(pid, 'exe');
|
|
75
|
+
const command = o.procReader(pid, 'cmdline');
|
|
76
|
+
const args = Buffer.from(command).toString('utf8').split('\0').filter(Boolean);
|
|
77
|
+
let scripts = args.filter((arg) => isAbsolute(arg));
|
|
78
|
+
const relative = args.filter((arg) => !arg.startsWith('-') && arg.includes('/') && !isAbsolute(arg));
|
|
79
|
+
if (relative.length) {
|
|
80
|
+
const cwd = String(o.procReader(pid, 'cwd'));
|
|
81
|
+
scripts.push(...relative.map((arg) => resolve(cwd, arg)));
|
|
82
|
+
}
|
|
83
|
+
const executable = String(exe).replace(/ \(deleted\)$/, '');
|
|
84
|
+
paths = basename(executable).startsWith('node') && !scripts.length ? [] :
|
|
85
|
+
[executable, ...scripts].map((path) => real(o.fs, path));
|
|
86
|
+
} catch { /* The live pid is still protected when its command is hidden. */ }
|
|
87
|
+
}
|
|
88
|
+
found.push({ pid, paths });
|
|
89
|
+
} catch { /* An unreadable lock is not evidence of a live daemon. */ }
|
|
90
|
+
}
|
|
91
|
+
return found;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function buildUninstallPlan(options = {}) {
|
|
95
|
+
const o = defaults(options);
|
|
96
|
+
const copies = [];
|
|
97
|
+
const add = (kind, path, removal = path, version) => {
|
|
98
|
+
if (!copies.some((c) => c.kind === kind && c.path === path)) copies.push({ kind, path, removal, ...(version ? { version } : {}) });
|
|
99
|
+
};
|
|
100
|
+
let npmRoot;
|
|
101
|
+
try { npmRoot = String((await o.execFile('npm', ['root', '-g'], { timeout: 5000 })).stdout).trim(); }
|
|
102
|
+
catch { /* npm may not be installed. */ }
|
|
103
|
+
const npmPackage = npmRoot && join(npmRoot, 'flowviant');
|
|
104
|
+
if (npmPackage && o.fs.existsSync(npmPackage)) add('npm-global', npmPackage, npmPackage, readVersion(o.fs, join(npmPackage, 'package.json')));
|
|
105
|
+
|
|
106
|
+
const npxRoot = join(o.home, '.npm', '_npx');
|
|
107
|
+
for (const hash of files(o.fs, npxRoot)) {
|
|
108
|
+
const dir = join(npxRoot, hash);
|
|
109
|
+
const pkg = join(dir, 'node_modules', 'flowviant');
|
|
110
|
+
if (!o.fs.existsSync(pkg)) continue;
|
|
111
|
+
const version = readVersion(o.fs, join(pkg, 'package.json'));
|
|
112
|
+
let alone = false;
|
|
113
|
+
try {
|
|
114
|
+
const meta = JSON.parse(o.fs.readFileSync(join(dir, 'package.json'), 'utf8'));
|
|
115
|
+
const names = Object.keys({ ...meta.dependencies, ...meta.devDependencies, ...meta.optionalDependencies });
|
|
116
|
+
alone = names.length === 1 && names[0] === 'flowviant';
|
|
117
|
+
} catch { /* Unknown cache contents must not be removed as a whole. */ }
|
|
118
|
+
add('npx-cache', pkg, dir, version);
|
|
119
|
+
copies.at(-1).skipReason = alone ? null : 'cache contains other packages or its dependencies could not be read';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const ownBin = join(o.home, '.flowviant', 'bin');
|
|
123
|
+
// Trusted: the installer's default directory and any directory an installer
|
|
124
|
+
// PATH block names. Anything else on PATH must prove itself.
|
|
125
|
+
const trusted = new Set([real(o.fs, ownBin)]);
|
|
126
|
+
const addBinary = (dir, fromPath = false) => {
|
|
127
|
+
const candidate = join(dir, 'flowviant');
|
|
128
|
+
if (!(fromPath ? executable(o.fs, candidate) : regular(o.fs, candidate))) return;
|
|
129
|
+
const target = real(o.fs, candidate);
|
|
130
|
+
if (inside(target, npxRoot)) return;
|
|
131
|
+
const packagePath = target.match(/^(.*\/node_modules\/flowviant)(?:\/|$)/)?.[1];
|
|
132
|
+
if (packagePath) {
|
|
133
|
+
if (!copies.some((c) => c.kind === 'npm-global' && real(o.fs, c.path) === real(o.fs, packagePath))) {
|
|
134
|
+
let npmPrefix = dirname(dirname(packagePath));
|
|
135
|
+
if (basename(npmPrefix) === 'lib') npmPrefix = dirname(npmPrefix);
|
|
136
|
+
const version = readVersion(o.fs, join(packagePath, 'package.json'));
|
|
137
|
+
copies.push({ kind: 'npm-global', path: packagePath, removal: packagePath,
|
|
138
|
+
npmPrefix, ...(version ? { version } : {}) });
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// Outside the installer's own directory a file must PROVE it is ours:
|
|
143
|
+
// compiled, and answering `--version` with a version (checked below).
|
|
144
|
+
const proven = trusted.has(real(o.fs, dir));
|
|
145
|
+
if (!proven && !compiledExecutable(o.fs, target)) return;
|
|
146
|
+
add('binary', target);
|
|
147
|
+
if (!proven) copies.at(-1).mustProve = true;
|
|
148
|
+
};
|
|
149
|
+
addBinary(join(o.home, '.flowviant', 'bin'));
|
|
150
|
+
for (const dir of o.path.split(':').filter(Boolean)) addBinary(dir, true);
|
|
151
|
+
for (const rc of ['.zshrc', '.bashrc', '.profile', '.config/fish/conf.d/flowviant.fish']) {
|
|
152
|
+
const path = join(o.home, rc);
|
|
153
|
+
let content;
|
|
154
|
+
try { content = o.fs.readFileSync(path, 'utf8'); } catch { continue; }
|
|
155
|
+
const pattern = basename(path) === 'flowviant.fish'
|
|
156
|
+
? /\n# flowviant\nfish_add_path "([^"\n]+)"\n/g
|
|
157
|
+
: /\n# flowviant\nexport PATH="([^"\n]+):\$PATH"\n/g;
|
|
158
|
+
for (const match of content.matchAll(pattern)) {
|
|
159
|
+
const dir = match[1];
|
|
160
|
+
if (match[0] !== exactBlock(dir, path)) continue;
|
|
161
|
+
copies.push({ kind: 'path-line', path, removal: path, block: match[0] });
|
|
162
|
+
trusted.add(real(o.fs, dir));
|
|
163
|
+
addBinary(dir);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const own = real(o.fs, o.compiled ? o.execPath : o.argvPath || '');
|
|
167
|
+
if (o.compiled && regular(o.fs, own)) add('binary', own);
|
|
168
|
+
for (const copy of copies.filter((c) => c.kind === 'binary')) {
|
|
169
|
+
if (o.compiled && copy.path === own) { copy.version = VERSION; continue; }
|
|
170
|
+
try {
|
|
171
|
+
const output = await o.execFile(copy.path, ['--version'], { timeout: 5000 });
|
|
172
|
+
const version = String(output.stdout).trim();
|
|
173
|
+
if (/^\d+\.\d+\.\d+/.test(version)) copy.version = version;
|
|
174
|
+
} catch { /* A foreign or older binary may not answer. */ }
|
|
175
|
+
}
|
|
176
|
+
for (let i = copies.length - 1; i >= 0; i--) {
|
|
177
|
+
if (copies[i].mustProve && !copies[i].version && !(o.compiled && copies[i].path === own)) copies.splice(i, 1);
|
|
178
|
+
else delete copies[i].mustProve;
|
|
179
|
+
}
|
|
180
|
+
const daemons = liveDaemons(o);
|
|
181
|
+
for (const copy of copies) {
|
|
182
|
+
const source = real(o.fs, copy.path);
|
|
183
|
+
copy.current = copy.kind === 'path-line' ? false :
|
|
184
|
+
(copy.kind === 'binary' ? source === own : inside(own, source));
|
|
185
|
+
const daemon = daemons.find((d) => !o.procAvailable || !d.paths.length || d.paths.some((p) =>
|
|
186
|
+
copy.kind === 'binary' ? p === source : inside(p, source)));
|
|
187
|
+
if (daemon && copy.kind !== 'path-line') copy.runningPid = daemon.pid;
|
|
188
|
+
}
|
|
189
|
+
return { copies, daemons, home: o.home };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function removePathBlock(o, path, block) {
|
|
193
|
+
const before = o.fs.readFileSync(path);
|
|
194
|
+
const at = before.indexOf(Buffer.from(block));
|
|
195
|
+
if (at < 0) return false;
|
|
196
|
+
const after = Buffer.concat([before.subarray(0, at), before.subarray(at + Buffer.byteLength(block))]);
|
|
197
|
+
const mode = o.fs.statSync(path).mode & 0o7777;
|
|
198
|
+
const temp = `${path}.${process.pid}.flowviant-tmp`;
|
|
199
|
+
try {
|
|
200
|
+
o.fs.writeFileSync(temp, after, { mode });
|
|
201
|
+
o.fs.chmodSync(temp, mode);
|
|
202
|
+
o.fs.renameSync(temp, path);
|
|
203
|
+
} finally { try { o.fs.rmSync(temp, { force: true }); } catch { /* Renamed. */ } }
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function runUninstall(options = {}) {
|
|
208
|
+
const o = defaults(options);
|
|
209
|
+
const { others = false, purge = false, yes = false, json = false } = options;
|
|
210
|
+
const result = { ok: true, removed: [], skipped: [], kept: [], errors: [] };
|
|
211
|
+
const line = (m) => o.log(m);
|
|
212
|
+
const plan = await buildUninstallPlan(o);
|
|
213
|
+
if (!json || !yes) {
|
|
214
|
+
for (const c of plan.copies) line(`${c.kind} ${c.path}${c.version ? ` (${c.version})` : ''}${c.runningPid ? ` (daemon pid ${c.runningPid})` : ''}`);
|
|
215
|
+
if (!plan.copies.length && !purge) line('no flowviant copies found on this machine.');
|
|
216
|
+
}
|
|
217
|
+
if (!yes && (plan.copies.length || purge)) {
|
|
218
|
+
if (!o.tty || json) result.errors.push('re-run with --yes to remove');
|
|
219
|
+
else if (!/^y(es)?$/i.test(await o.prompt?.('Remove these? [y/N] ') || '')) result.errors.push('cancelled');
|
|
220
|
+
if (result.errors.length) {
|
|
221
|
+
result.ok = false;
|
|
222
|
+
if (json) o.stdout(JSON.stringify(result)); else result.errors.forEach(line);
|
|
223
|
+
return result;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!others) {
|
|
228
|
+
try {
|
|
229
|
+
const stop = o.stopAllDaemons || (await import('./instance.mjs')).stopAllDaemons;
|
|
230
|
+
const tally = await stop({ log: line });
|
|
231
|
+
if (tally.failed) result.errors.push(`${tally.failed} daemon(s) could not be stopped`);
|
|
232
|
+
} catch (e) { result.errors.push(`stopping daemons: ${errorWords(e)}`); }
|
|
233
|
+
}
|
|
234
|
+
if (purge && !others) {
|
|
235
|
+
try {
|
|
236
|
+
const projects = o.projects || (await import('./credentials.mjs')).listStoredProjects;
|
|
237
|
+
const disconnect = o.disconnect || (async (entry, log) => {
|
|
238
|
+
const { disconnectHere, realDisconnectDeps, leaveUrlFrom } = await import('./machines.mjs');
|
|
239
|
+
const { FLEET_URL } = await import('./config.mjs');
|
|
240
|
+
return disconnectHere(entry, await realDisconnectDeps({ url: leaveUrlFrom(FLEET_URL), log }), { log });
|
|
241
|
+
});
|
|
242
|
+
for (const entry of await projects()) {
|
|
243
|
+
try {
|
|
244
|
+
const messages = [];
|
|
245
|
+
const res = await disconnect(entry, (m) => { messages.push(m); line(m); });
|
|
246
|
+
const failure = messages.find((m) => /could not tell the app|does not take a leave|not disconnected|could not forget/.test(m));
|
|
247
|
+
if (!res.ok || failure) result.errors.push(`registry removal for ${entry.projectId}: ${failure || messages.at(-1) || 'failed'}`);
|
|
248
|
+
} catch (e) { result.errors.push(`registry removal for ${entry.projectId}: ${errorWords(e)}`); }
|
|
249
|
+
}
|
|
250
|
+
} catch (e) { result.errors.push(`reading projects: ${errorWords(e)}`); }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
for (const c of plan.copies) {
|
|
254
|
+
const item = { kind: c.kind, path: c.path };
|
|
255
|
+
let reason = c.skipReason;
|
|
256
|
+
if (others && c.current) reason = 'running copy';
|
|
257
|
+
if (others && c.runningPid) reason = `a daemon is running from it (pid ${c.runningPid})`;
|
|
258
|
+
if (others && c.kind === 'path-line' && plan.daemons.length && !o.procAvailable) reason = 'a daemon may be using this copy';
|
|
259
|
+
if (others && c.kind === 'path-line' && plan.copies.some((copy) => copy.kind === 'binary' && copy.current && c.block === exactBlock(dirname(copy.path), c.path))) reason = 'needed by the running binary';
|
|
260
|
+
if (reason) {
|
|
261
|
+
result.skipped.push({ ...item, reason });
|
|
262
|
+
// The copy doing the work is not news; every other skip is.
|
|
263
|
+
if (!json && reason !== 'running copy') line(`skipped ${c.path}: ${reason}`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
if (c.kind === 'npm-global') await o.execFile('npm',
|
|
268
|
+
c.npmPrefix ? ['uninstall', '-g', '--prefix', c.npmPrefix, 'flowviant'] : ['uninstall', '-g', 'flowviant'],
|
|
269
|
+
{ timeout: 120000 });
|
|
270
|
+
else if (c.kind === 'path-line') removePathBlock(o, c.path, c.block);
|
|
271
|
+
else o.fs.rmSync(c.removal, { recursive: c.kind === 'npx-cache', force: true });
|
|
272
|
+
result.removed.push(item);
|
|
273
|
+
if (!json) line(c.kind === 'path-line' ? `removed the flowviant PATH line from ${c.path}` : `removed ${c.path}`);
|
|
274
|
+
} catch (e) { result.errors.push(`${c.path}: ${errorWords(e)}`); }
|
|
275
|
+
}
|
|
276
|
+
if (purge && !others) {
|
|
277
|
+
try { o.fs.rmSync(join(o.home, '.flowviant'), { recursive: true, force: true }); }
|
|
278
|
+
catch (e) { result.errors.push(`local purge: ${errorWords(e)}`); }
|
|
279
|
+
} else {
|
|
280
|
+
result.kept.push(join(o.home, '.flowviant'));
|
|
281
|
+
if (!json && !others) line('Kept logins and local state in ~/.flowviant; reinstalling resumes them.');
|
|
282
|
+
}
|
|
283
|
+
result.ok = result.errors.length === 0;
|
|
284
|
+
for (const error of result.errors) if (!json) line(`error: ${error}`);
|
|
285
|
+
if (json) o.stdout(JSON.stringify(result));
|
|
286
|
+
return result;
|
|
287
|
+
}
|
package/bin/lib/update.mjs
CHANGED
|
@@ -271,6 +271,13 @@ export async function handleVersionSignal({ latest, min, autoUpdate, safeToUpdat
|
|
|
271
271
|
note(`flowviant ${cur} → ${target}: downloading binary…`);
|
|
272
272
|
const installed = await installBinaryUpdate({ minimumVersion: target });
|
|
273
273
|
ok('updated — restarting into the new version.');
|
|
274
|
+
// The replacement is committed. Older copies are cleaned by the NEW
|
|
275
|
+
// binary in a detached child: in-process, the re-exec below could cut an
|
|
276
|
+
// `npm uninstall` off halfway and leave a broken global package. The
|
|
277
|
+
// child skips any copy a daemon still runs from.
|
|
278
|
+
try {
|
|
279
|
+
spawn(process.execPath, ['uninstall', '--others', '--yes', '--json'], { detached: true, stdio: 'ignore' }).unref();
|
|
280
|
+
} catch (e) { warn(`older-copy cleanup: ${e?.message ?? e}`); }
|
|
274
281
|
reexec(teardown, { viaBinary: true, target: installed });
|
|
275
282
|
return true;
|
|
276
283
|
} catch (e) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.101.2",
|
|
4
4
|
"description": "Run your own coding CLIs as build agents for Flowviant — Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|