@yassimba/pi-loom 0.2.0
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/LICENSE +21 -0
- package/README.md +39 -0
- package/index.ts +1 -0
- package/package.json +53 -0
- package/src/index.ts +64 -0
- package/src/update.ts +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# pi-loom
|
|
2
|
+
|
|
3
|
+
Loom's woven logo in Pi's header, with a startup notice when a newer Loom CLI is ready to install.
|
|
4
|
+
|
|
5
|
+
## Try it
|
|
6
|
+
|
|
7
|
+
From a checkout of this repository, with Pi 0.85.1 or newer:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pi -e ./plugins/pi-loom/index.ts
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
To keep it installed:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pi install ./plugins/pi-loom
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Restart Pi after installing to run the startup check. The package is also registered in Loom's setup catalog for publication; it is not added to existing installations automatically.
|
|
20
|
+
|
|
21
|
+
## Behavior
|
|
22
|
+
|
|
23
|
+
- Replaces Pi's header with a purple, 24-column Unicode braille rendering of the supplied Loom logo, with the name aligned at the top. Narrow terminals show just **Loom**. Pi's other UI and native update notices remain unchanged.
|
|
24
|
+
- At interactive process startup, runs `loom --version` and reads the Loom executable pin from the [published manifest](https://github.com/Yassimba/loom/blob/main/manifest/loom.toml). It does not use GitHub's latest release, which could belong to a different component.
|
|
25
|
+
- If that stable version is newer, shows: `Loom update available: VERSION. Run loom update or use the /loom skill to update.` The skill must be installed separately; `loom update` always remains the direct command.
|
|
26
|
+
- Never installs anything or changes Pi settings. Checks run in the background, with a 1.5-second command timeout and a four-second overall deadline. Missing Loom, unrecognized/development versions, and network failures stay quiet.
|
|
27
|
+
- Skips checks in headless modes and with `PI_OFFLINE=1`, `true`, or `yes`. Switching, forking, or reloading sessions rebrands the header without repeating the check. Shutdown cancels pending work and suppresses late notices.
|
|
28
|
+
|
|
29
|
+
Only one extension can own Pi's header; another header extension may replace this one. Remove pi-loom with `pi remove ./plugins/pi-loom` (from the same checkout) and reload to return to Pi's normal header.
|
|
30
|
+
|
|
31
|
+
## Development
|
|
32
|
+
|
|
33
|
+
From the repository root:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
node --experimental-strip-types --test test/pi-loom.test.ts
|
|
37
|
+
npm run check
|
|
38
|
+
npm run audit
|
|
39
|
+
```
|
package/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from "./src/index.ts";
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yassimba/pi-loom",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Loom branding and a startup notice for published Loom updates in Pi.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.6"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"pi-package",
|
|
12
|
+
"pi",
|
|
13
|
+
"loom"
|
|
14
|
+
],
|
|
15
|
+
"files": [
|
|
16
|
+
"index.ts",
|
|
17
|
+
"src",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/Yassimba/loom.git",
|
|
24
|
+
"directory": "plugins/pi-loom"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"pi": {
|
|
30
|
+
"extensions": [
|
|
31
|
+
"./index.ts"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"loom": {
|
|
35
|
+
"catalog": {
|
|
36
|
+
"label": "Loom",
|
|
37
|
+
"description": "Loom branding and a startup notice for published Loom updates in Pi.",
|
|
38
|
+
"nextAction": "Start Pi to see the Loom header and available Loom updates."
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
43
|
+
"@earendil-works/pi-tui": "*"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"@earendil-works/pi-coding-agent": {
|
|
47
|
+
"optional": true
|
|
48
|
+
},
|
|
49
|
+
"@earendil-works/pi-tui": {
|
|
50
|
+
"optional": true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { checkLoomUpdate, execVersionCommand } from "./update.ts";
|
|
4
|
+
|
|
5
|
+
// Frozen 24-column braille rendering of assets/loom-logo.svg; no runtime rasterizer.
|
|
6
|
+
const LOGO = [
|
|
7
|
+
" ⢠⣴⣦⡀",
|
|
8
|
+
" ⢀⣴⣿⣿⣿⣿⣦⡀",
|
|
9
|
+
" ⢶⣿⣿⣿⠋⠙⣿⣿⣿⡦",
|
|
10
|
+
" ⢀⣤⡀ ⠙⢿⡿⣢⣾⣿⡿⠋ ⢀⣤⡀",
|
|
11
|
+
" ⢀⣴⣿⣿⣿⣦⡀⢠⣾⣿⡿⢋⣄⢀⣴⣿⣿⣿⣦⡀",
|
|
12
|
+
"⢀⣴⣿⣿⡿⠛⢿⣿⣿⣎⠻⠋⠐⢿⣿⣷⣝⠟⠙⢿⣿⣿⣦⡀",
|
|
13
|
+
"⠙⢿⣿⣿⣷⣄⣴⣝⢿⣿⣷⠄⣠⣦⡙⢿⣿⣷⣤⣾⣿⣿⡿⠋",
|
|
14
|
+
" ⠙⢿⣿⣿⡿⠋ ⠉⣡⣾⣿⡿⠁ ⠙⢿⣿⣿⡿⠋",
|
|
15
|
+
" ⠙⠋ ⣠⣾⣿⡿⢫⣾⣷⣄ ⠙⠋",
|
|
16
|
+
" ⠺⣿⣿⣿⣄⣠⣿⣿⣿⠗",
|
|
17
|
+
" ⠈⠻⣿⣿⣿⣿⠟⠁",
|
|
18
|
+
" ⠘⢿⠟⠁",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export default function (pi: ExtensionAPI, exec = execVersionCommand) {
|
|
22
|
+
let checked = false;
|
|
23
|
+
const shutdown = new AbortController();
|
|
24
|
+
|
|
25
|
+
pi.on("session_start", (event, ctx) => {
|
|
26
|
+
if (ctx.mode !== "tui" || !ctx.hasUI) return;
|
|
27
|
+
ctx.ui.setHeader((_tui, theme) => ({
|
|
28
|
+
render(width) {
|
|
29
|
+
if (width < 48) return [truncateToWidth(theme.bold("Loom"), width, "")];
|
|
30
|
+
const labels = [
|
|
31
|
+
theme.bold("Loom"),
|
|
32
|
+
theme.fg("muted", "Your opinionated agent setup."),
|
|
33
|
+
"",
|
|
34
|
+
`${theme.bold("/loom")} ${theme.fg("dim", "Set up, add tools, or update.")}`,
|
|
35
|
+
];
|
|
36
|
+
return LOGO.map((line, index) => {
|
|
37
|
+
const label = labels[index] ?? "";
|
|
38
|
+
// Match the SVG's #8038E9 independently of the active Pi theme.
|
|
39
|
+
return truncateToWidth(
|
|
40
|
+
`\x1b[38;2;128;56;233m${line.padEnd(28)}\x1b[39m${label}`,
|
|
41
|
+
width,
|
|
42
|
+
"",
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
invalidate() {},
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
// Pi reconstructs extensions on reload/new/resume/fork. Only process startup
|
|
50
|
+
// checks; branding still applies to every replacement session.
|
|
51
|
+
if (checked || event.reason !== "startup") return;
|
|
52
|
+
checked = true;
|
|
53
|
+
void checkLoomUpdate(exec, shutdown.signal).then((version) => {
|
|
54
|
+
if (version && !shutdown.signal.aborted) {
|
|
55
|
+
ctx.ui.notify(
|
|
56
|
+
`Loom update available: ${version}. Run loom update or use the /loom skill to update.`,
|
|
57
|
+
"info",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
pi.on("session_shutdown", () => shutdown.abort());
|
|
64
|
+
}
|
package/src/update.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
// Pi's exec timeout only sends SIGTERM; a wrapper can ignore it indefinitely.
|
|
6
|
+
export const execVersionCommand: ExtensionAPI["exec"] = async (command, args, options) => {
|
|
7
|
+
const { stdout, stderr } = await promisify(execFile)(command, args, {
|
|
8
|
+
signal: options?.signal,
|
|
9
|
+
timeout: options?.timeout,
|
|
10
|
+
killSignal: "SIGKILL",
|
|
11
|
+
maxBuffer: 4096,
|
|
12
|
+
encoding: "utf8",
|
|
13
|
+
});
|
|
14
|
+
return { stdout, stderr, code: 0, killed: false };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const MANIFEST_URL = "https://raw.githubusercontent.com/Yassimba/loom/main/manifest/loom.toml";
|
|
18
|
+
const VERSION = String.raw`(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)`;
|
|
19
|
+
|
|
20
|
+
export function availableLoomVersion(installed: string, manifest: string): string | undefined {
|
|
21
|
+
const current = installed.trim().match(new RegExp(`^loom (${VERSION})$`));
|
|
22
|
+
// The published manifest requires one tool per line (also used by the installers).
|
|
23
|
+
// Match only the Loom executable in [tools], never another component's release.
|
|
24
|
+
const tools = manifest.split(/^\[tools\]\s*$/m)[1]?.split(/^\[/m)[0];
|
|
25
|
+
const latest = tools?.match(
|
|
26
|
+
new RegExp(
|
|
27
|
+
String.raw`^"github:Yassimba/loom\[exe=loom\]"\s*=\s*\{\s*version\s*=\s*"loom-v(${VERSION})"[^\r\n]*\}\s*(?:#.*)?$`,
|
|
28
|
+
"m",
|
|
29
|
+
),
|
|
30
|
+
);
|
|
31
|
+
if (!current || !latest) return undefined;
|
|
32
|
+
for (let index = 2; index <= 4; index++) {
|
|
33
|
+
const difference = BigInt(latest[index]) - BigInt(current[index]);
|
|
34
|
+
if (difference !== 0n) return difference > 0n ? latest[1] : undefined;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function checkLoomUpdate(
|
|
40
|
+
exec: ExtensionAPI["exec"],
|
|
41
|
+
shutdown: AbortSignal,
|
|
42
|
+
): Promise<string | undefined> {
|
|
43
|
+
if (/^(1|true|yes)$/i.test(process.env.PI_OFFLINE?.trim() ?? "")) return undefined;
|
|
44
|
+
const signal = AbortSignal.any([shutdown, AbortSignal.timeout(4000)]);
|
|
45
|
+
try {
|
|
46
|
+
const result = await exec("loom", ["--version"], { timeout: 1500, signal });
|
|
47
|
+
if (result.code !== 0 || result.killed || signal.aborted) return undefined;
|
|
48
|
+
if (!new RegExp(`^loom ${VERSION}$`).test(result.stdout.trim())) return undefined;
|
|
49
|
+
const response = await fetch(MANIFEST_URL, { signal });
|
|
50
|
+
if (!response.ok) return undefined;
|
|
51
|
+
return availableLoomVersion(result.stdout, await response.text());
|
|
52
|
+
} catch {
|
|
53
|
+
// Missing Loom, offline hosts, and failed checks must not interrupt startup.
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|