pi-remote-control 1.0.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/README.md +46 -0
- package/docs/adr/0001-package-extension-as-control-shim.md +19 -0
- package/docs/adr/0002-use-sqlite-for-daemon-state.md +19 -0
- package/docs/adr/0003-use-lock-file-as-process-state.md +19 -0
- package/docs/adr/0004-allow-loopback-pair-code-without-token.md +19 -0
- package/docs/adr/0005-defer-os-service-installation.md +19 -0
- package/docs/adr/0006-use-tui-activated-remote-control-sessions.md +24 -0
- package/docs/adr/0007-require-tui-originated-pairing.md +19 -0
- package/docs/adr/0008-use-qr-pairing-links.md +21 -0
- package/docs/adr/0009-rename-package-to-remote-control.md +19 -0
- package/docs/adr/0010-clean-stale-lock-on-status.md +19 -0
- package/docs/adr/0011-use-loopback-tui-control.md +19 -0
- package/docs/adr/0012-use-paginated-session-transcript-loading.md +37 -0
- package/docs/adr/0013-require-manual-reactivation-after-tui-entry.md +31 -0
- package/docs/adr/0014-read-transcripts-from-session-files.md +33 -0
- package/docs/adr/0015-normalize-transcript-messages-and-stream-events.md +35 -0
- package/docs/adr/0016-expose-turn-lifecycle-events.md +31 -0
- package/docs/adr/0017-bound-initial-websocket-session-state.md +31 -0
- package/docs/adr/0018-reregister-active-tui-session-on-heartbeat-miss.md +33 -0
- package/docs/adr/0019-display-only-pairing-qr-and-expiry.md +25 -0
- package/docs/adr/0020-expose-session-status-snapshots.md +31 -0
- package/docs/adr/0021-support-remote-compact-action.md +31 -0
- package/docs/adr/0022-rename-session-status-to-runtime-status.md +27 -0
- package/docs/adr/0023-return-remote-compact-results.md +29 -0
- package/docs/architecture.md +96 -0
- package/docs/data-model.md +284 -0
- package/docs/interfaces.md +470 -0
- package/package.json +37 -0
- package/scripts/http-smoke-test.sh +100 -0
- package/src/active-session-registry.ts +205 -0
- package/src/auth/pairing.ts +30 -0
- package/src/auth/tokens.ts +30 -0
- package/src/cli-runner.cjs +15 -0
- package/src/cli.ts +254 -0
- package/src/config.ts +26 -0
- package/src/extension/index.ts +422 -0
- package/src/index.ts +16 -0
- package/src/lock.ts +26 -0
- package/src/pairing-link.ts +15 -0
- package/src/paths.ts +21 -0
- package/src/persistence/daemon-store.ts +56 -0
- package/src/persistence/schema.ts +21 -0
- package/src/qr.ts +23 -0
- package/src/runtime-status.ts +116 -0
- package/src/server/http.ts +529 -0
- package/src/session-index.ts +9 -0
- package/src/session-transcript.ts +34 -0
- package/src/transcript-message.ts +76 -0
- package/src/transcript-pagination.ts +68 -0
- package/src/transcript-preview.ts +102 -0
- package/src/transcript-stream.ts +89 -0
- package/src/types.ts +116 -0
- package/tests/active-session-registry.test.ts +170 -0
- package/tests/auth.test.ts +18 -0
- package/tests/cli.test.ts +361 -0
- package/tests/config.test.ts +35 -0
- package/tests/daemon-store.test.ts +54 -0
- package/tests/extension.test.ts +617 -0
- package/tests/lock.test.ts +36 -0
- package/tests/pairing-link.test.ts +26 -0
- package/tests/pairing.test.ts +26 -0
- package/tests/paths.test.ts +29 -0
- package/tests/qr.test.ts +25 -0
- package/tests/schema.test.ts +18 -0
- package/tests/server-http.test.ts +932 -0
- package/tests/session-index.test.ts +10 -0
- package/tests/session-transcript.test.ts +75 -0
- package/tests/transcript-pagination.test.ts +54 -0
- package/tests/transcript-preview.test.ts +64 -0
- package/tests/transcript-stream.test.ts +103 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { canClaimPairingCode, createPairingCode, hashPairingCode } from "../src/auth/pairing.js";
|
|
3
|
+
|
|
4
|
+
describe("pairing codes", () => {
|
|
5
|
+
it("creates a six digit code without storing it in plaintext", () => {
|
|
6
|
+
const pairing = createPairingCode(new Date("2026-05-09T00:00:00.000Z"), 60_000);
|
|
7
|
+
|
|
8
|
+
expect(pairing.rawCode).toMatch(/^\d{6}$/);
|
|
9
|
+
expect(pairing.codeHash).not.toContain(pairing.rawCode);
|
|
10
|
+
expect(pairing.expiresAt).toBe("2026-05-09T00:01:00.000Z");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("hashes the same raw code consistently", () => {
|
|
14
|
+
expect(hashPairingCode("123456")).toBe(hashPairingCode("123456"));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("allows only matching, unconsumed, unexpired codes", () => {
|
|
18
|
+
const now = new Date("2026-05-09T00:00:00.000Z");
|
|
19
|
+
const pairing = createPairingCode(now, 60_000);
|
|
20
|
+
|
|
21
|
+
expect(canClaimPairingCode(pairing, pairing.rawCode, now)).toBe(true);
|
|
22
|
+
expect(canClaimPairingCode(pairing, "000000", now)).toBe(false);
|
|
23
|
+
expect(canClaimPairingCode({ ...pairing, consumedAt: now.toISOString() }, pairing.rawCode, now)).toBe(false);
|
|
24
|
+
expect(canClaimPairingCode(pairing, pairing.rawCode, new Date("2026-05-09T00:02:00.000Z"))).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { mkdtemp, stat, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { ensureDaemonStateDir, getDaemonStateDir } from "../src/paths.js";
|
|
6
|
+
|
|
7
|
+
describe("daemon state directory", () => {
|
|
8
|
+
it("uses PI_REMOTE_CONTROL_DIR when provided", () => {
|
|
9
|
+
expect(getDaemonStateDir({ env: { PI_REMOTE_CONTROL_DIR: "./state" }, homeDir: "/home/test" })).toMatch(/state$/);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("defaults to ~/.pi/remote-control", () => {
|
|
13
|
+
expect(getDaemonStateDir({ env: {}, homeDir: "/home/test" })).toBe("/home/test/.pi/remote-control");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("creates the state directory", async () => {
|
|
17
|
+
const root = await mkdtemp(join(tmpdir(), "pi-remote-control-"));
|
|
18
|
+
const stateDir = join(root, "state");
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
await ensureDaemonStateDir(stateDir);
|
|
22
|
+
const info = await stat(stateDir);
|
|
23
|
+
expect(info.isDirectory()).toBe(true);
|
|
24
|
+
expect(info.mode & 0o077).toBe(0);
|
|
25
|
+
} finally {
|
|
26
|
+
await rm(root, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
});
|
package/tests/qr.test.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { formatPairingDisplay, renderPairingQr } from "../src/qr.js";
|
|
3
|
+
|
|
4
|
+
describe("pairing QR display", () => {
|
|
5
|
+
it("renders a terminal QR code for a pairing link", () => {
|
|
6
|
+
const qr = renderPairingQr("pi-remote://pair?code=123456");
|
|
7
|
+
|
|
8
|
+
expect(qr).toContain("█");
|
|
9
|
+
expect(qr.split("\n").length).toBeGreaterThan(5);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("formats only QR and expiry", () => {
|
|
13
|
+
const display = formatPairingDisplay({
|
|
14
|
+
expiresAt: "2026-05-09T00:01:00.000Z",
|
|
15
|
+
pairingLink: "pi-remote://pair?code=123456",
|
|
16
|
+
qrCode: "QR-CODE",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(display).toEqual([
|
|
20
|
+
"Scan with Pi iOS app:",
|
|
21
|
+
"QR-CODE",
|
|
22
|
+
"Expires at: 2026-05-09T00:01:00.000Z",
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createSchemaSql, migrateSchemaSql, SCHEMA_VERSION } from "../src/persistence/schema.js";
|
|
3
|
+
|
|
4
|
+
describe("database schema", () => {
|
|
5
|
+
it("declares the expected initial tables", () => {
|
|
6
|
+
const sql = createSchemaSql().join("\n");
|
|
7
|
+
|
|
8
|
+
expect(sql).toContain("create table if not exists meta");
|
|
9
|
+
expect(sql).toContain("create table if not exists devices");
|
|
10
|
+
expect(sql).toContain("create table if not exists pairing_codes");
|
|
11
|
+
expect(sql).not.toContain("create table if not exists projects");
|
|
12
|
+
expect(sql).not.toContain("create table if not exists session_index");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("creates initial schema from version zero", () => {
|
|
16
|
+
expect(migrateSchemaSql(0, SCHEMA_VERSION).length).toBeGreaterThan(0);
|
|
17
|
+
});
|
|
18
|
+
});
|