@timqi/pier 0.0.3 → 0.0.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 +16 -12
- package/dist/cli.js +18 -5
- package/dist/db.js +77 -20
- package/dist/main.js +18 -10
- package/dist/service.js +27 -5
- package/dist/web/explorer.js +3 -3
- package/dist/web/public/assets/index-BAW9Nhaa.js +90 -0
- package/dist/web/public/assets/index-CwBoxtXP.css +2 -0
- package/dist/web/public/index.html +2 -2
- package/dist/web/server.js +18 -1
- package/docs/deploy.md +47 -22
- package/package.json +1 -1
- package/dist/web/public/assets/index-cCIuQnDr.css +0 -2
- package/dist/web/public/assets/index-fASxMPr6.js +0 -90
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ versioned from `0.0.1` on — earlier databases are not migrated. Read
|
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
28
|
npm install -g @timqi/pier
|
|
29
|
-
pier
|
|
29
|
+
pier serve
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
It listens on `127.0.0.1:3141` (`PORT`, `HOST`) and keeps everything under
|
|
@@ -57,7 +57,7 @@ installation. By default it is `$PIER_HOME/pi` (`~/.pier/pi`). Set
|
|
|
57
57
|
an existing Pi setup:
|
|
58
58
|
|
|
59
59
|
```sh
|
|
60
|
-
PI_CODING_AGENT_DIR="$HOME/.pi/agent" pier
|
|
60
|
+
PI_CODING_AGENT_DIR="$HOME/.pi/agent" pier serve
|
|
61
61
|
```
|
|
62
62
|
|
|
63
63
|
Console → Settings is the normal setup path:
|
|
@@ -68,8 +68,9 @@ Console → Settings is the normal setup path:
|
|
|
68
68
|
- **Agent files** edits `SYSTEM.md`, `AGENTS.md`, `settings.json`, and advanced
|
|
69
69
|
`models.json` structure in the Pi agent directory — globally, or per project
|
|
70
70
|
scope, where it also shows that project's `.pi/skills` and `.pi/extensions`
|
|
71
|
-
resources. Changes apply when a session next opens;
|
|
72
|
-
idle,
|
|
71
|
+
resources. Changes apply when a session next opens; saving here recycles the
|
|
72
|
+
idle ones for you, and **Settings → Instance → Reload** does it for files
|
|
73
|
+
something else changed — an agent, or an editor on the box.
|
|
73
74
|
|
|
74
75
|
On first credential access, Pier imports an existing `auth.json` into its sealed
|
|
75
76
|
store and renames the source to `auth.json.imported`. Literal provider keys left
|
|
@@ -109,8 +110,9 @@ find a version-managed one), a memory drop-in it never rewrites afterwards, and
|
|
|
109
110
|
turns on linger so scheduled tasks survive your logout. Install also records the
|
|
110
111
|
exact npm executable in a separate updater unit. Re-run `pier service install
|
|
111
112
|
--force` after changing the service settings or its Node/npm installation; this
|
|
112
|
-
rewrites both units and restarts Pier. On macOS run `pier` in a terminal,
|
|
113
|
-
under whatever supervisor you already use
|
|
113
|
+
rewrites both units and restarts Pier. On macOS run `pier serve` in a terminal,
|
|
114
|
+
or under whatever supervisor you already use — `pier` on its own only prints
|
|
115
|
+
the usage.
|
|
114
116
|
|
|
115
117
|
`docs/deploy.md` is the same thing written out by hand, plus what the memory
|
|
116
118
|
limits mean, how updates work (and why the updater is a second unit), how to
|
|
@@ -154,13 +156,15 @@ time to finish, whatever the deadline still had to cut off written to the chat
|
|
|
154
156
|
it belonged to — and only then hand over. The automatic path additionally waits
|
|
155
157
|
for an idle instance: no turn streaming, no task run in flight.
|
|
156
158
|
|
|
157
|
-
Either way the updater writes `~/.pier/db/pier.db.release
|
|
158
|
-
touches the package
|
|
159
|
-
|
|
159
|
+
Either way the updater writes `~/.pier/db/backups/pier.db.release-<version>.bak`
|
|
160
|
+
before npm touches the package — named for the release being replaced, which is
|
|
161
|
+
the one to reinstall beside it; the three newest are kept. Then it updates the
|
|
162
|
+
npm installation recorded when the service was installed, and starts Pier again.
|
|
160
163
|
|
|
161
|
-
`main` is the only development line. `
|
|
162
|
-
tag
|
|
163
|
-
|
|
164
|
+
`main` is the only development line. `just release [patch|minor|major]` runs the
|
|
165
|
+
checks, writes the tag and pushes it; the tag builds and publishes to npm and as
|
|
166
|
+
a GitHub Release. The version in the web footer is the one from `package.json`
|
|
167
|
+
— so the number on screen always names a commit.
|
|
164
168
|
Schema upgrades are one-way: a database migrated by a newer Pier is refused by
|
|
165
169
|
an older one. The release backup above is the way back; `docs/deploy.md` has the
|
|
166
170
|
restore procedure and the additional snapshots taken before schema migrations.
|
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ const version = currentVersion();
|
|
|
15
15
|
const HELP = `pier ${version} — a self-hosted workspace for coding agents
|
|
16
16
|
|
|
17
17
|
Usage
|
|
18
|
-
pier
|
|
18
|
+
pier serve run the workbench in this terminal
|
|
19
19
|
pier service install write and start a systemd user unit (Linux)
|
|
20
20
|
pier service uninstall stop it and remove the unit
|
|
21
21
|
pier service status what systemd thinks of it
|
|
@@ -76,7 +76,15 @@ else if (values.version || command === "version") {
|
|
|
76
76
|
process.stdout.write(`${version}\n`);
|
|
77
77
|
}
|
|
78
78
|
else if (!command) {
|
|
79
|
-
|
|
79
|
+
// Typing the bare name is how someone finds out what this is, so it answers
|
|
80
|
+
// that and nothing else: it used to start a server, which is a surprising
|
|
81
|
+
// amount to have done by accident.
|
|
82
|
+
process.stdout.write(HELP);
|
|
83
|
+
}
|
|
84
|
+
else if (command === "serve") {
|
|
85
|
+
if (subcommand)
|
|
86
|
+
fail(`unexpected argument "${subcommand}"`);
|
|
87
|
+
allowOnly([], "pier serve");
|
|
80
88
|
// The server starts on import; this file stays a dispatcher.
|
|
81
89
|
await import("./main.js");
|
|
82
90
|
}
|
|
@@ -170,7 +178,9 @@ async function signalService(command) {
|
|
|
170
178
|
}
|
|
171
179
|
async function backup() {
|
|
172
180
|
const [{ backupDb }, { PIER_DB }] = await Promise.all([import("./db.js"), import("./paths.js")]);
|
|
173
|
-
|
|
181
|
+
// This tree's version: the updater runs `backup` before npm replaces it, so
|
|
182
|
+
// it is the release the copy pairs with.
|
|
183
|
+
const path = backupDb(version, PIER_DB);
|
|
174
184
|
process.stdout.write(path ? `backed up ${path}\n` : `no database yet — nothing to back up.\n`);
|
|
175
185
|
}
|
|
176
186
|
function commandPath(name) {
|
|
@@ -191,8 +201,8 @@ async function service(action = "status") {
|
|
|
191
201
|
const systemdAction = action === "install" || action === "uninstall";
|
|
192
202
|
if (systemdAction && process.platform !== "linux") {
|
|
193
203
|
process.stderr.write(`pier service is systemd, so Linux only — this is ${process.platform}.\n` +
|
|
194
|
-
`Run "pier" in a terminal, or under whatever supervisor you already
|
|
195
|
-
`it
|
|
204
|
+
`Run "pier serve" in a terminal, or under whatever supervisor you already\n` +
|
|
205
|
+
`use; it takes no arguments and keeps its state in $PIER_HOME (~/.pier).\n`);
|
|
196
206
|
process.exit(2);
|
|
197
207
|
}
|
|
198
208
|
switch (action) {
|
|
@@ -211,6 +221,9 @@ async function service(action = "status") {
|
|
|
211
221
|
if (!install({
|
|
212
222
|
execPath: process.execPath,
|
|
213
223
|
npmPath: commandPath("npm"),
|
|
224
|
+
// This command is typed in the operator's shell, so its PATH is the one
|
|
225
|
+
// they expect a turn's commands to see; the unit records it.
|
|
226
|
+
shellPath: process.env.PATH,
|
|
214
227
|
entry: fileURLToPath(new URL("./main.js", import.meta.url)),
|
|
215
228
|
host,
|
|
216
229
|
port,
|
package/dist/db.js
CHANGED
|
@@ -11,15 +11,16 @@
|
|
|
11
11
|
// So: one connection, one ordered list of migrations, applied in one
|
|
12
12
|
// transaction before any store exists. A store receives the handle and owns
|
|
13
13
|
// only its queries.
|
|
14
|
-
import { chmodSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "node:fs";
|
|
14
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, statSync } from "node:fs";
|
|
15
15
|
import { basename, dirname, join } from "node:path";
|
|
16
16
|
import { DatabaseSync } from "node:sqlite";
|
|
17
17
|
import { logger } from "./log.js";
|
|
18
18
|
import { PIER_DB } from "./paths.js";
|
|
19
19
|
const log = logger("db");
|
|
20
|
-
/**
|
|
20
|
+
/** Snapshots to keep *of each kind*. Three is two upgrades of regret plus one:
|
|
21
21
|
* they are full copies of the database, and the one that matters is the
|
|
22
|
-
* newest.
|
|
22
|
+
* newest. Counted per kind because the two kinds answer different questions —
|
|
23
|
+
* a run of releases must not evict the pre-migration copies. */
|
|
23
24
|
const KEEP_BACKUPS = 3;
|
|
24
25
|
/** How long a second process may wait for the write lock before failing. Two
|
|
25
26
|
* Pier processes on one PIER_HOME contend exactly once — at boot, when both
|
|
@@ -147,14 +148,25 @@ let shared;
|
|
|
147
148
|
*/
|
|
148
149
|
export const pierDb = () => (shared ??= openDb(PIER_DB));
|
|
149
150
|
/** A release-level restore point, taken while the service is stopped even when
|
|
150
|
-
* the release has no schema migration. The previous complete
|
|
151
|
-
* writing
|
|
152
|
-
|
|
151
|
+
* the release has no schema migration. The previous complete copies stay put if
|
|
152
|
+
* writing this one fails.
|
|
153
|
+
*
|
|
154
|
+
* `version` is the Pier that produced this database, not the one being
|
|
155
|
+
* installed: the updater runs this from the tree it is about to replace, and
|
|
156
|
+
* restoring a database means reinstalling the code that speaks its schema
|
|
157
|
+
* (`migrate` refuses one from a newer Pier). So the name carries the other half
|
|
158
|
+
* of the pair. Backing up twice at one version replaces that version's copy —
|
|
159
|
+
* the pairing is identical, so a second name for it would say nothing. */
|
|
160
|
+
export function backupDb(version, path = PIER_DB) {
|
|
153
161
|
if (!existsSync(path))
|
|
154
162
|
return undefined;
|
|
155
|
-
|
|
163
|
+
// In a filename, so it may not carry a separator or a traversal; a version
|
|
164
|
+
// this malformed is a broken install, not something to guess at.
|
|
165
|
+
const safe = version.replaceAll(/[^0-9A-Za-z.+-]/g, "_") || "unknown";
|
|
166
|
+
const bak = join(backupsDir(path, true), `${basename(path)}.release-${safe}.bak`);
|
|
156
167
|
copyDatabase(path, bak);
|
|
157
168
|
log.info(`pre-update backup: ${bak}`);
|
|
169
|
+
prune(releases(path));
|
|
158
170
|
return bak;
|
|
159
171
|
}
|
|
160
172
|
/** Open a database, bring it to the current schema, and lock down its files.
|
|
@@ -186,9 +198,9 @@ function migrate(db, path, migrations) {
|
|
|
186
198
|
if (at > target) {
|
|
187
199
|
// Name the snapshot that exists rather than a pattern: the operator is
|
|
188
200
|
// reading this because the service will not start.
|
|
189
|
-
const newest = path === ":memory:" ? undefined :
|
|
201
|
+
const newest = path === ":memory:" ? undefined : snapshots(path)[0]?.file;
|
|
190
202
|
throw new Error(`${path} is at schema ${at}, this Pier speaks ${target}: a database is ` +
|
|
191
|
-
`never downgraded. Restore ${newest ??
|
|
203
|
+
`never downgraded. Restore ${newest ?? `a copy from ${backupsDir(path)}`}, or run the newer Pier.`);
|
|
192
204
|
}
|
|
193
205
|
// Version 0 with tables is a database from before versioning existed.
|
|
194
206
|
// Migration 1 assumes an empty file, so the collision it would hit says
|
|
@@ -244,7 +256,7 @@ function migrate(db, path, migrations) {
|
|
|
244
256
|
}
|
|
245
257
|
log.info(locked === 0 ? `schema created at version ${target}` : `schema ${locked} → ${target}`);
|
|
246
258
|
if (path !== ":memory:")
|
|
247
|
-
prune(path);
|
|
259
|
+
prune(snapshots(path).map(({ file }) => file));
|
|
248
260
|
}
|
|
249
261
|
/**
|
|
250
262
|
* The copy that exists because `user_version` only counts up: the transaction
|
|
@@ -259,7 +271,7 @@ function migrate(db, path, migrations) {
|
|
|
259
271
|
* ever refers to a finished copy.
|
|
260
272
|
*/
|
|
261
273
|
function snapshot(path, at) {
|
|
262
|
-
const bak = `${path}.v${at}.bak
|
|
274
|
+
const bak = join(backupsDir(path, true), `${basename(path)}.v${at}.bak`);
|
|
263
275
|
copyDatabase(path, bak);
|
|
264
276
|
log.info(`pre-migration backup: ${bak}`);
|
|
265
277
|
}
|
|
@@ -276,22 +288,64 @@ function copyDatabase(path, bak) {
|
|
|
276
288
|
chmodSync(tmp, 0o600); // it holds everything the 0600 database holds
|
|
277
289
|
renameSync(tmp, bak);
|
|
278
290
|
}
|
|
279
|
-
/**
|
|
280
|
-
|
|
291
|
+
/**
|
|
292
|
+
* One directory for every copy of this database, `db/backups/`. Beside the
|
|
293
|
+
* database was fine while there was one snapshot per schema; a restore point
|
|
294
|
+
* per release turns that into a listing where the live file and its sidecars
|
|
295
|
+
* are hard to pick out, and "which of these do I not delete" is the wrong
|
|
296
|
+
* question to make an operator answer under pressure.
|
|
297
|
+
*
|
|
298
|
+
* `create` also adopts what an older Pier wrote next to the database, so the
|
|
299
|
+
* restore procedure names one location instead of two forever.
|
|
300
|
+
*/
|
|
301
|
+
function backupsDir(path, create = false) {
|
|
302
|
+
const dir = join(dirname(path), "backups");
|
|
303
|
+
if (!create)
|
|
304
|
+
return dir;
|
|
305
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
306
|
+
const prefix = `${basename(path)}.`;
|
|
307
|
+
for (const name of readdirSync(dirname(path))) {
|
|
308
|
+
if (!name.startsWith(prefix) || !name.endsWith(".bak"))
|
|
309
|
+
continue;
|
|
310
|
+
renameSync(join(dirname(path), name), join(dir, name));
|
|
311
|
+
log.info(`moved ${name} into ${dir}`);
|
|
312
|
+
}
|
|
313
|
+
return dir;
|
|
314
|
+
}
|
|
315
|
+
/** The copies of one kind: `v<schema>` or `release-<version>`. Disjoint
|
|
316
|
+
* prefixes, so each kind is counted and pruned on its own. */
|
|
317
|
+
function listBackups(path, kind) {
|
|
318
|
+
const dir = backupsDir(path);
|
|
319
|
+
if (!existsSync(dir))
|
|
320
|
+
return [];
|
|
321
|
+
const prefix = `${basename(path)}.${kind}`;
|
|
322
|
+
return readdirSync(dir).filter((name) => name.startsWith(prefix) && name.endsWith(".bak"));
|
|
323
|
+
}
|
|
324
|
+
/** Pre-migration snapshots, newest schema first — the number in the name is an
|
|
325
|
+
* ordinal, so it orders them without asking the filesystem. */
|
|
326
|
+
function snapshots(path) {
|
|
281
327
|
const prefix = `${basename(path)}.v`;
|
|
282
|
-
return
|
|
283
|
-
.filter((name) => name.startsWith(prefix) && name.endsWith(".bak"))
|
|
328
|
+
return listBackups(path, "v")
|
|
284
329
|
.map((name) => ({
|
|
285
330
|
version: Number(name.slice(prefix.length, -".bak".length)),
|
|
286
|
-
file: join(
|
|
331
|
+
file: join(backupsDir(path), name),
|
|
287
332
|
}))
|
|
288
333
|
.filter(({ version }) => Number.isInteger(version))
|
|
289
334
|
.sort((a, b) => b.version - a.version);
|
|
290
335
|
}
|
|
291
|
-
/**
|
|
292
|
-
*
|
|
293
|
-
|
|
294
|
-
|
|
336
|
+
/** Release restore points, newest copy first. Ordered by mtime: the name holds
|
|
337
|
+
* a Pier version, and comparing those means reimplementing semver here — while
|
|
338
|
+
* two updates of one instance are never in flight at the same moment. Legacy
|
|
339
|
+
* `pier.db.release.bak` shares the prefix, so it ages out like the rest. */
|
|
340
|
+
function releases(path) {
|
|
341
|
+
return listBackups(path, "release")
|
|
342
|
+
.map((name) => join(backupsDir(path), name))
|
|
343
|
+
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
|
|
344
|
+
}
|
|
345
|
+
/** Keep the newest few, oldest first out. Nobody restores a database from four
|
|
346
|
+
* upgrades ago, and every one of these is the size of the whole database. */
|
|
347
|
+
function prune(newestFirst) {
|
|
348
|
+
for (const file of newestFirst.slice(KEEP_BACKUPS)) {
|
|
295
349
|
rmSync(file, { force: true });
|
|
296
350
|
log.info(`removed superseded backup: ${file}`);
|
|
297
351
|
}
|
|
@@ -311,4 +365,7 @@ function restrict(path) {
|
|
|
311
365
|
chmodSync(file, 0o600);
|
|
312
366
|
}
|
|
313
367
|
chmodSync(dirname(path), 0o700);
|
|
368
|
+
// Full copies of the same secrets, one directory down.
|
|
369
|
+
if (existsSync(backupsDir(path)))
|
|
370
|
+
chmodSync(backupsDir(path), 0o700);
|
|
314
371
|
}
|
package/dist/main.js
CHANGED
|
@@ -135,6 +135,16 @@ const startChannels = async () => {
|
|
|
135
135
|
await deliverLedger(restartLedger, (entry) => channels.notify(entry.channelId, entry.conversationId, entry.note))
|
|
136
136
|
.catch((err) => log.error("restart-note delivery failed", err));
|
|
137
137
|
};
|
|
138
|
+
/** What "reload" means, in one place: the adapters re-read their configuration
|
|
139
|
+
* and sessions are let go, so the next message re-opens them with the current
|
|
140
|
+
* skills, extensions, prompts and credentials — all applied at attach, none
|
|
141
|
+
* stored in a transcript. SIGHUP (`pier reload`) and the Console's Reload are
|
|
142
|
+
* both this call; `includeWatched` is the only difference, and only because the
|
|
143
|
+
* Console knows a person asked from the session they are looking at. */
|
|
144
|
+
const reloadInstance = async (includeWatched = false) => {
|
|
145
|
+
await channels.reload();
|
|
146
|
+
return router.evictIdle(0, Date.now(), { includeWatched });
|
|
147
|
+
};
|
|
138
148
|
void secrets.unlock().then(startChannels, (err) => log.error("secrets locked — channels not started; unlock from Console → Settings → Security, or repair master.key", err));
|
|
139
149
|
// Replacing Pier is systemd's job, not this process's: the oneshot unit stops
|
|
140
150
|
// the service, snapshots the database, installs and starts it again. Without
|
|
@@ -261,6 +271,7 @@ app.route("/", createServer({
|
|
|
261
271
|
updater,
|
|
262
272
|
// Unlocked from the Console: start the channels boot held back.
|
|
263
273
|
onUnlocked: () => void startChannels(),
|
|
274
|
+
reload: () => reloadInstance(true),
|
|
264
275
|
backgroundRuns: (id) => tasks.backgroundRuns(id),
|
|
265
276
|
}));
|
|
266
277
|
const port = Number(process.env.PORT ?? 3141);
|
|
@@ -327,19 +338,16 @@ process.on("SIGUSR2", () => {
|
|
|
327
338
|
.catch((err) => log.error("drain failed — shutting down anyway", err))
|
|
328
339
|
.then(() => shutdown(false));
|
|
329
340
|
});
|
|
330
|
-
// Reload without a restart (`pier reload`):
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
// a foreground `pier` keeps SIGHUP's default, dying with its terminal instead
|
|
336
|
-
// of surviving as an orphan that holds the port.
|
|
341
|
+
// Reload without a restart (`pier reload`): reloadInstance above, leaving the
|
|
342
|
+
// sessions someone is watching alone — nobody asked from a browser here.
|
|
343
|
+
// Only under systemd (the CLI signals through systemctl): a foreground `pier
|
|
344
|
+
// serve` keeps SIGHUP's default, dying with its terminal instead of surviving
|
|
345
|
+
// as an orphan that holds the port.
|
|
337
346
|
if (process.env.INVOCATION_ID) {
|
|
338
347
|
process.on("SIGHUP", () => {
|
|
339
348
|
log.info("SIGHUP received, reloading channels and recycling idle sessions");
|
|
340
|
-
void
|
|
341
|
-
void router.evictIdle(0)
|
|
349
|
+
void reloadInstance()
|
|
342
350
|
.then((n) => log.info(`recycled ${String(n)} idle session(s)`))
|
|
343
|
-
.catch((err) => log.error("
|
|
351
|
+
.catch((err) => log.error("reload failed", err));
|
|
344
352
|
});
|
|
345
353
|
}
|
package/dist/service.js
CHANGED
|
@@ -36,8 +36,22 @@ function quote(value, command = false) {
|
|
|
36
36
|
return `"${escaped}"`;
|
|
37
37
|
}
|
|
38
38
|
const environment = (key, value) => `Environment=${quote(`${key}=${value}`)}`;
|
|
39
|
+
/** The PATH both units carry: the recorded node first, then the shell that ran
|
|
40
|
+
* the install (`pier service install` is typed in that shell, so its own PATH
|
|
41
|
+
* *is* the login one), with the standard directories as a floor.
|
|
42
|
+
*
|
|
43
|
+
* Recorded at install rather than sourced from a login shell at start, which
|
|
44
|
+
* would hand a dotfile the power to decide whether Pier boots and which node
|
|
45
|
+
* npm installs into. Relative entries are dropped: they would resolve against
|
|
46
|
+
* WorkingDirectory, which is not where the operator was standing. */
|
|
47
|
+
function pathEnv(execPath, shellPath) {
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
return [dirname(execPath), ...(shellPath ?? "").split(":"), "/usr/local/bin", "/usr/bin", "/bin"]
|
|
50
|
+
.filter((dir) => dir.startsWith("/") && !/[\0\r\n]/.test(dir) && !seen.has(dir) && seen.add(dir))
|
|
51
|
+
.join(":");
|
|
52
|
+
}
|
|
39
53
|
export function renderUnit(options) {
|
|
40
|
-
const { execPath, entry, host, port, pierHome } = options;
|
|
54
|
+
const { execPath, entry, host, port, pierHome, shellPath } = options;
|
|
41
55
|
return `[Unit]
|
|
42
56
|
Description=Pier — agent workspace
|
|
43
57
|
Documentation=https://github.com/timqi/pier
|
|
@@ -51,6 +65,10 @@ WorkingDirectory=%h
|
|
|
51
65
|
# that installed Pier is usually not on it.
|
|
52
66
|
ExecStart=${quote(execPath, true)} ${quote(entry, true)}
|
|
53
67
|
${environment("NODE_ENV", "production")}
|
|
68
|
+
# Inherited by every command a turn runs, which is why it is here and not just
|
|
69
|
+
# in the updater: an agent typing "npm test" on systemd's minimal PATH would be
|
|
70
|
+
# told node does not exist on a machine that installed Pier with it.
|
|
71
|
+
${environment("PATH", pathEnv(execPath, shellPath))}
|
|
54
72
|
# Loopback by default. Put a reverse proxy in front before widening this —
|
|
55
73
|
# whoever reaches this port can drive an agent that runs a shell.
|
|
56
74
|
${environment("HOST", host)}
|
|
@@ -89,7 +107,7 @@ function legacyOptions(home) {
|
|
|
89
107
|
/** A separate cgroup stops Pier, takes a consistent backup, updates the exact
|
|
90
108
|
* npm installation recorded at install time, and always starts Pier again. */
|
|
91
109
|
export function renderUpdateUnit(options) {
|
|
92
|
-
const { execPath, npmPath, entry, pierHome } = options;
|
|
110
|
+
const { execPath, npmPath, entry, pierHome, shellPath } = options;
|
|
93
111
|
const cli = join(dirname(entry), "cli.js");
|
|
94
112
|
return `[Unit]
|
|
95
113
|
Description=Update Pier to the latest published version
|
|
@@ -97,10 +115,14 @@ Documentation=https://github.com/timqi/pier
|
|
|
97
115
|
|
|
98
116
|
[Service]
|
|
99
117
|
Type=oneshot
|
|
118
|
+
# The absolute node below answers npm's own shebang and nothing else: a
|
|
119
|
+
# dependency's postinstall runs as "sh -c node scripts/postinstall", which
|
|
120
|
+
# resolves node from PATH, and systemd's minimal PATH has no fnm/nvm node —
|
|
121
|
+
# the install then dies with "node: not found" with the tree half written.
|
|
122
|
+
${environment("PATH", pathEnv(execPath, shellPath))}
|
|
100
123
|
${pierHome ? `${environment("PIER_HOME", pierHome)}\n` : ""}ExecStart=systemctl --user stop ${UNIT_NAME}
|
|
101
124
|
ExecStart=${quote(execPath, true)} ${quote(cli, true)} backup
|
|
102
|
-
# npm runs under the recorded node: its shebang needs a node on PATH
|
|
103
|
-
# systemd's minimal PATH has none for fnm/nvm installs.
|
|
125
|
+
# npm runs under the recorded node: its shebang needs a node on PATH too.
|
|
104
126
|
ExecStart=${quote(execPath, true)} ${quote(npmPath, true)} install -g @timqi/pier@latest
|
|
105
127
|
# ExecStopPost runs on success and failure, so a failed backup or npm install
|
|
106
128
|
# does not leave the previously working service stopped.
|
|
@@ -278,7 +300,7 @@ export function startUpdate(options) {
|
|
|
278
300
|
if (!run(["systemctl", "--user", "start", "--no-block", UPDATE_UNIT_NAME]))
|
|
279
301
|
return "failed";
|
|
280
302
|
say(`updating in the background — follow it with: journalctl --user -u ${UPDATE_UNIT_NAME} -f`);
|
|
281
|
-
say(`Pier stops, snapshots
|
|
303
|
+
say(`Pier stops, snapshots the database into db/backups/, installs, then starts again.`);
|
|
282
304
|
return "started";
|
|
283
305
|
}
|
|
284
306
|
export function uninstall(home = homedir(), say = console.log, exec) {
|
package/dist/web/explorer.js
CHANGED
|
@@ -92,13 +92,13 @@ export function registerExplorerRoutes(app) {
|
|
|
92
92
|
return { name: line.slice(0, tab), subject: line.slice(tab + 1) };
|
|
93
93
|
});
|
|
94
94
|
// Unit/record separators, because a body is multi-line by nature.
|
|
95
|
-
const commits = (await git(root, "log", "-20", "--format=%h\u001f%at\u001f%an\u001f%s\u001f%b\u001e"))
|
|
95
|
+
const commits = (await git(root, "log", "-20", "--format=%h\u001f%at\u001f%an\u001f%ae\u001f%s\u001f%b\u001e"))
|
|
96
96
|
.split("\u001e")
|
|
97
97
|
.map((r) => r.trimStart())
|
|
98
98
|
.filter(Boolean)
|
|
99
99
|
.map((r) => {
|
|
100
|
-
const [hash = "", at = "", author = "", subject = "", body = ""] = r.split("\u001f");
|
|
101
|
-
return { hash, at: Number(at) * 1000, author, subject, body: body.trim() };
|
|
100
|
+
const [hash = "", at = "", author = "", email = "", subject = "", body = ""] = r.split("\u001f");
|
|
101
|
+
return { hash, at: Number(at) * 1000, author, email, subject, body: body.trim() };
|
|
102
102
|
});
|
|
103
103
|
return c.json({ branch, refs, commits });
|
|
104
104
|
});
|