@hayasaka7/haya-pet 0.1.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/.gitattributes +34 -0
- package/.github/workflows/release.yml +61 -0
- package/LICENSE +21 -0
- package/README.md +247 -0
- package/apps/cli/src/haya-pet.js +395 -0
- package/apps/cli/test/haya-pet.test.mjs +339 -0
- package/apps/companion/README.md +83 -0
- package/apps/companion/package.json +17 -0
- package/apps/companion/src/main/display-manager.js +71 -0
- package/apps/companion/src/main/index.js +349 -0
- package/apps/companion/src/main/lock-file.js +52 -0
- package/apps/companion/src/main/panel-placement.js +45 -0
- package/apps/companion/src/main/pet-loader.js +2 -0
- package/apps/companion/src/main/position-store.js +3 -0
- package/apps/companion/src/main/preload.cjs +13 -0
- package/apps/companion/src/main/state-file.js +2 -0
- package/apps/companion/src/main/terminal-helper-client.js +79 -0
- package/apps/companion/src/main/terminal-locator.js +44 -0
- package/apps/companion/src/main/tray-menu.js +79 -0
- package/apps/companion/src/main/window-options.js +66 -0
- package/apps/companion/src/renderer/index.html +18 -0
- package/apps/companion/src/renderer/interaction-controller.js +114 -0
- package/apps/companion/src/renderer/pet-window.js +275 -0
- package/apps/companion/src/renderer/session-bubbles.js +138 -0
- package/apps/companion/src/renderer/styles.css +225 -0
- package/apps/companion/src/renderer/task-talk-window.js +141 -0
- package/apps/companion/test/display-manager.test.mjs +48 -0
- package/apps/companion/test/interaction-controller.test.mjs +107 -0
- package/apps/companion/test/panel-placement.test.mjs +60 -0
- package/apps/companion/test/position-store.test.mjs +54 -0
- package/apps/companion/test/state-file.test.mjs +52 -0
- package/apps/companion/test/terminal-helper-client.test.mjs +68 -0
- package/apps/companion/test/terminal-locator.test.mjs +35 -0
- package/apps/companion/test/tray-menu.test.mjs +45 -0
- package/apps/companion/test/window-options.test.mjs +62 -0
- package/apps/pet-preview/index.html +42 -0
- package/apps/pet-preview/src/preview-app.js +123 -0
- package/apps/pet-preview/src/preview-state.js +70 -0
- package/apps/pet-preview/src/preview.css +125 -0
- package/apps/pet-preview/test/preview-state.test.mjs +62 -0
- package/assets/fallback-pet/README.md +16 -0
- package/assets/fallback-pet/pet.json +13 -0
- package/docs/architecture.md +144 -0
- package/docs/known-issues.md +49 -0
- package/docs/publishing.md +48 -0
- package/docs/screenshots/README.md +7 -0
- package/docs/screenshots/folder-collapsed.png +0 -0
- package/docs/screenshots/hero.png +0 -0
- package/docs/screenshots/pet-overlay.png +0 -0
- package/docs/screenshots/session-bubbles.png +0 -0
- package/docs/screenshots/tray-menu.png +0 -0
- package/docs/troubleshooting.md +36 -0
- package/native/README.md +80 -0
- package/native/linux-window-helper/README.md +29 -0
- package/native/mac-window-helper/README.md +30 -0
- package/native/win-window-helper/Program.cs +312 -0
- package/native/win-window-helper/README.md +53 -0
- package/native/win-window-helper/win-window-helper.csproj +12 -0
- package/package.json +35 -0
- package/packages/adapters/src/adapter-info.js +61 -0
- package/packages/adapters/src/capabilities.js +39 -0
- package/packages/adapters/src/heuristics.js +114 -0
- package/packages/adapters/src/output-observer.js +164 -0
- package/packages/adapters/src/routing.js +86 -0
- package/packages/adapters/test/adapter-info.test.mjs +35 -0
- package/packages/adapters/test/capabilities.test.mjs +44 -0
- package/packages/adapters/test/heuristics.test.mjs +42 -0
- package/packages/adapters/test/output-observer.test.mjs +142 -0
- package/packages/adapters/test/routing.test.mjs +93 -0
- package/packages/app-state/src/state-file.js +53 -0
- package/packages/app-state/src/state.js +80 -0
- package/packages/app-state/test/state.test.mjs +36 -0
- package/packages/cli-core/src/companion-launcher.js +69 -0
- package/packages/cli-core/src/pty-runner.js +96 -0
- package/packages/cli-core/src/run-command.js +353 -0
- package/packages/cli-core/src/strip-ansi.js +16 -0
- package/packages/cli-core/test/companion-launcher.test.mjs +98 -0
- package/packages/cli-core/test/run-command.test.mjs +177 -0
- package/packages/cli-core/test/strip-ansi.test.mjs +27 -0
- package/packages/daemon-core/src/daemon-runtime.js +49 -0
- package/packages/daemon-core/src/ipc-server.js +180 -0
- package/packages/daemon-core/src/ipc-transport.js +70 -0
- package/packages/daemon-core/src/singleton.js +46 -0
- package/packages/daemon-core/test/daemon-runtime.test.mjs +65 -0
- package/packages/daemon-core/test/ipc-server.test.mjs +70 -0
- package/packages/daemon-core/test/ipc-transport.test.mjs +72 -0
- package/packages/daemon-core/test/singleton.test.mjs +32 -0
- package/packages/pet-core/src/animation-state.js +84 -0
- package/packages/pet-core/src/animator.js +26 -0
- package/packages/pet-core/src/atlas.js +81 -0
- package/packages/pet-core/src/discovery.js +90 -0
- package/packages/pet-core/src/manifest.js +112 -0
- package/packages/pet-core/src/validation.js +43 -0
- package/packages/pet-core/test/animation-state.test.mjs +47 -0
- package/packages/pet-core/test/animator.test.mjs +31 -0
- package/packages/pet-core/test/atlas.test.mjs +81 -0
- package/packages/pet-core/test/discovery.test.mjs +93 -0
- package/packages/pet-core/test/manifest.test.mjs +93 -0
- package/packages/pet-core/test/validation.test.mjs +69 -0
- package/packages/platform-core/src/capabilities.js +49 -0
- package/packages/platform-core/src/paths.js +75 -0
- package/packages/platform-core/src/platform.js +15 -0
- package/packages/platform-core/test/platform.test.mjs +84 -0
- package/packages/protocol/src/messages.js +156 -0
- package/packages/protocol/test/messages.test.mjs +112 -0
- package/packages/session-core/src/bubble-linger.js +47 -0
- package/packages/session-core/src/bubble-view.js +79 -0
- package/packages/session-core/src/pet-state.js +56 -0
- package/packages/session-core/src/priority.js +56 -0
- package/packages/session-core/src/registry.js +144 -0
- package/packages/session-core/src/summaries.js +54 -0
- package/packages/session-core/test/bubble-linger.test.mjs +96 -0
- package/packages/session-core/test/bubble-view.test.mjs +79 -0
- package/packages/session-core/test/pet-state.test.mjs +118 -0
- package/packages/session-core/test/priority.test.mjs +53 -0
- package/packages/session-core/test/registry.test.mjs +161 -0
- package/packages/session-core/test/summaries.test.mjs +38 -0
- package/packages/task-core/src/approvals.js +91 -0
- package/packages/task-core/src/controls.js +61 -0
- package/packages/task-core/src/replies.js +80 -0
- package/packages/task-core/src/task-events.js +101 -0
- package/packages/task-core/src/task-status.js +93 -0
- package/packages/task-core/src/task-store.js +74 -0
- package/packages/task-core/test/approvals.test.mjs +61 -0
- package/packages/task-core/test/controls.test.mjs +61 -0
- package/packages/task-core/test/replies.test.mjs +51 -0
- package/packages/task-core/test/task-events.test.mjs +67 -0
- package/packages/task-core/test/task-status.test.mjs +49 -0
- package/packages/task-core/test/task-store.test.mjs +65 -0
- package/test/harness.mjs +22 -0
- package/test/run-tests.mjs +47 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "../../../test/harness.mjs";
|
|
3
|
+
import {
|
|
4
|
+
clearDragAction,
|
|
5
|
+
createAnimationState,
|
|
6
|
+
resolveCurrentAction,
|
|
7
|
+
setDragAction,
|
|
8
|
+
setStableAction,
|
|
9
|
+
triggerOneShot
|
|
10
|
+
} from "../src/animation-state.js";
|
|
11
|
+
|
|
12
|
+
test("stable action loops until it is changed", () => {
|
|
13
|
+
let state = createAnimationState("idle");
|
|
14
|
+
|
|
15
|
+
assert.equal(resolveCurrentAction(state, 1000), "idle");
|
|
16
|
+
|
|
17
|
+
state = setStableAction(state, "review");
|
|
18
|
+
|
|
19
|
+
assert.equal(resolveCurrentAction(state, 1200), "review");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("one-shot action temporarily overrides stable action", () => {
|
|
23
|
+
const state = triggerOneShot(createAnimationState("review"), "waving", 1000, 500);
|
|
24
|
+
|
|
25
|
+
assert.equal(resolveCurrentAction(state, 1499), "waving");
|
|
26
|
+
assert.equal(resolveCurrentAction(state, 1500), "review");
|
|
27
|
+
assert.equal(state.previousStableAction, "review");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("stable action changes become the return target after an active one-shot", () => {
|
|
31
|
+
let state = triggerOneShot(createAnimationState("idle"), "jumping", 1000, 500);
|
|
32
|
+
state = setStableAction(state, "running");
|
|
33
|
+
|
|
34
|
+
assert.equal(resolveCurrentAction(state, 1250), "jumping");
|
|
35
|
+
assert.equal(resolveCurrentAction(state, 1500), "running");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("drag action overrides both stable and one-shot actions", () => {
|
|
39
|
+
let state = triggerOneShot(createAnimationState("review"), "waving", 1000, 500);
|
|
40
|
+
state = setDragAction(state, "left");
|
|
41
|
+
|
|
42
|
+
assert.equal(resolveCurrentAction(state, 1100), "running-left");
|
|
43
|
+
|
|
44
|
+
state = clearDragAction(state);
|
|
45
|
+
|
|
46
|
+
assert.equal(resolveCurrentAction(state, 1100), "waving");
|
|
47
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "../../../test/harness.mjs";
|
|
3
|
+
import { getFrameAt, getActionDurationMs } from "../src/animator.js";
|
|
4
|
+
|
|
5
|
+
const manifest = { frameDurationMs: 100, actionFrameDurations: { idle: 200 } };
|
|
6
|
+
|
|
7
|
+
test("computes the active frame index from elapsed time", () => {
|
|
8
|
+
assert.equal(getFrameAt("running", 0, manifest), 0);
|
|
9
|
+
assert.equal(getFrameAt("running", 250, manifest), 2);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("loops frames within the action frame count", () => {
|
|
13
|
+
// running has 6 frames at 100ms -> 600ms wraps back to 0
|
|
14
|
+
assert.equal(getFrameAt("running", 600, manifest), 0);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("honours per-action frame duration overrides", () => {
|
|
18
|
+
// idle override is 200ms; 500ms -> frame 2
|
|
19
|
+
assert.equal(getFrameAt("idle", 500, manifest), 2);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("computes the full duration of an action loop", () => {
|
|
23
|
+
// waving has 4 frames at 100ms
|
|
24
|
+
assert.equal(getActionDurationMs("waving", manifest), 400);
|
|
25
|
+
// idle has 6 frames at 200ms override
|
|
26
|
+
assert.equal(getActionDurationMs("idle", manifest), 1200);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("rejects unknown actions", () => {
|
|
30
|
+
assert.throws(() => getFrameAt("dancing", 0, manifest), /Unknown pet action/);
|
|
31
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "../../../test/harness.mjs";
|
|
3
|
+
import {
|
|
4
|
+
ACTION_ROWS,
|
|
5
|
+
ATLAS_HEIGHT,
|
|
6
|
+
ATLAS_WIDTH,
|
|
7
|
+
CELL_HEIGHT,
|
|
8
|
+
CELL_WIDTH,
|
|
9
|
+
FRAME_COUNTS,
|
|
10
|
+
getFrameCount,
|
|
11
|
+
getFrameRect,
|
|
12
|
+
mapAiStateToPetAction
|
|
13
|
+
} from "../src/atlas.js";
|
|
14
|
+
|
|
15
|
+
test("defines Codex-compatible atlas geometry and action rows", () => {
|
|
16
|
+
assert.equal(CELL_WIDTH, 192);
|
|
17
|
+
assert.equal(CELL_HEIGHT, 208);
|
|
18
|
+
assert.equal(ATLAS_WIDTH, 1536);
|
|
19
|
+
assert.equal(ATLAS_HEIGHT, 1872);
|
|
20
|
+
|
|
21
|
+
assert.deepEqual(ACTION_ROWS, {
|
|
22
|
+
idle: 0,
|
|
23
|
+
"running-right": 1,
|
|
24
|
+
"running-left": 2,
|
|
25
|
+
waving: 3,
|
|
26
|
+
jumping: 4,
|
|
27
|
+
failed: 5,
|
|
28
|
+
waiting: 6,
|
|
29
|
+
running: 7,
|
|
30
|
+
review: 8
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
assert.deepEqual(FRAME_COUNTS, {
|
|
34
|
+
idle: 6,
|
|
35
|
+
"running-right": 8,
|
|
36
|
+
"running-left": 8,
|
|
37
|
+
waving: 4,
|
|
38
|
+
jumping: 5,
|
|
39
|
+
failed: 8,
|
|
40
|
+
waiting: 6,
|
|
41
|
+
running: 6,
|
|
42
|
+
review: 6
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("returns frame rectangles for valid pet actions", () => {
|
|
47
|
+
assert.deepEqual(getFrameRect("running-right", 3), {
|
|
48
|
+
x: 576,
|
|
49
|
+
y: 208,
|
|
50
|
+
width: 192,
|
|
51
|
+
height: 208
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
assert.deepEqual(getFrameRect("review", 5), {
|
|
55
|
+
x: 960,
|
|
56
|
+
y: 1664,
|
|
57
|
+
width: 192,
|
|
58
|
+
height: 208
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("rejects unknown actions and out-of-range frame indexes", () => {
|
|
63
|
+
assert.throws(() => getFrameCount("dancing"), /Unknown pet action/);
|
|
64
|
+
assert.throws(() => getFrameRect("idle", 6), /Frame index 6 is out of range/);
|
|
65
|
+
assert.throws(() => getFrameRect("waving", -1), /Frame index -1 is out of range/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("maps normalized AI states to pet actions", () => {
|
|
69
|
+
assert.equal(mapAiStateToPetAction("idle"), "idle");
|
|
70
|
+
assert.equal(mapAiStateToPetAction("thinking"), "review");
|
|
71
|
+
assert.equal(mapAiStateToPetAction("running_tool"), "running");
|
|
72
|
+
assert.equal(mapAiStateToPetAction("editing_files"), "running");
|
|
73
|
+
assert.equal(mapAiStateToPetAction("waiting_user"), "waiting");
|
|
74
|
+
assert.equal(mapAiStateToPetAction("waiting_approval"), "waiting");
|
|
75
|
+
assert.equal(mapAiStateToPetAction("reviewing"), "review");
|
|
76
|
+
assert.equal(mapAiStateToPetAction("compacting"), "review");
|
|
77
|
+
assert.equal(mapAiStateToPetAction("failed"), "failed");
|
|
78
|
+
assert.equal(mapAiStateToPetAction("success"), "jumping");
|
|
79
|
+
assert.equal(mapAiStateToPetAction("stale"), "waiting");
|
|
80
|
+
assert.equal(mapAiStateToPetAction("exited"), "jumping");
|
|
81
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { test } from "../../../test/harness.mjs";
|
|
4
|
+
import { discoverPets, loadPetFromDir } from "../src/discovery.js";
|
|
5
|
+
|
|
6
|
+
function manifest(id, name) {
|
|
7
|
+
return JSON.stringify({ id, name, spritesheet: "spritesheet.webp" });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Builds an injectable fake fs from explicit dir listings and file contents.
|
|
11
|
+
function fakeFs({ dirs = {}, files = {} } = {}) {
|
|
12
|
+
return {
|
|
13
|
+
async readdir(dir) {
|
|
14
|
+
if (!(dir in dirs)) {
|
|
15
|
+
const error = new Error("no dir");
|
|
16
|
+
error.code = "ENOENT";
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
return dirs[dir];
|
|
20
|
+
},
|
|
21
|
+
async readFile(path) {
|
|
22
|
+
if (!(path in files)) {
|
|
23
|
+
const error = new Error("no file");
|
|
24
|
+
error.code = "ENOENT";
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
return files[path];
|
|
28
|
+
},
|
|
29
|
+
async stat(path) {
|
|
30
|
+
if (!(path in files)) {
|
|
31
|
+
const error = new Error("no file");
|
|
32
|
+
error.code = "ENOENT";
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("discovers pets with a manifest and spritesheet", async () => {
|
|
41
|
+
const root = join("petsdir");
|
|
42
|
+
const catDir = join(root, "cat");
|
|
43
|
+
const fs = fakeFs({
|
|
44
|
+
dirs: { [root]: ["cat"] },
|
|
45
|
+
files: {
|
|
46
|
+
[join(catDir, "pet.json")]: manifest("cat", "Cat"),
|
|
47
|
+
[join(catDir, "spritesheet.webp")]: "binary"
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const pets = await discoverPets([root], fs);
|
|
52
|
+
assert.equal(pets.length, 1);
|
|
53
|
+
assert.equal(pets[0].manifest.id, "cat");
|
|
54
|
+
assert.equal(pets[0].manifest.name, "Cat");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("skips folders missing a spritesheet", async () => {
|
|
58
|
+
const root = join("petsdir");
|
|
59
|
+
const fs = fakeFs({
|
|
60
|
+
dirs: { [root]: ["nopix"] },
|
|
61
|
+
files: { [join(root, "nopix", "pet.json")]: manifest("nopix", "NoPix") }
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
assert.deepEqual(await discoverPets([root], fs), []);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("dedupes by id across search paths (first wins)", async () => {
|
|
68
|
+
const a = join("a");
|
|
69
|
+
const b = join("b");
|
|
70
|
+
const fs = fakeFs({
|
|
71
|
+
dirs: { [a]: ["cat"], [b]: ["cat"] },
|
|
72
|
+
files: {
|
|
73
|
+
[join(a, "cat", "pet.json")]: manifest("cat", "Cat A"),
|
|
74
|
+
[join(a, "cat", "spritesheet.webp")]: "x",
|
|
75
|
+
[join(b, "cat", "pet.json")]: manifest("cat", "Cat B"),
|
|
76
|
+
[join(b, "cat", "spritesheet.webp")]: "x"
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const pets = await discoverPets([a, b], fs);
|
|
81
|
+
assert.equal(pets.length, 1);
|
|
82
|
+
assert.equal(pets[0].manifest.name, "Cat A");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("returns undefined for an invalid manifest", async () => {
|
|
86
|
+
const dir = join("p");
|
|
87
|
+
const fs = fakeFs({ files: { [join(dir, "pet.json")]: "{ not json" } });
|
|
88
|
+
assert.equal(await loadPetFromDir(dir, fs), undefined);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("tolerates missing search directories", async () => {
|
|
92
|
+
assert.deepEqual(await discoverPets([join("does-not-exist")], fakeFs()), []);
|
|
93
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "../../../test/harness.mjs";
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_FRAME_DURATION_MS,
|
|
5
|
+
getFrameDurationMs,
|
|
6
|
+
parsePetManifest
|
|
7
|
+
} from "../src/manifest.js";
|
|
8
|
+
|
|
9
|
+
test("parses a minimal pet manifest with defaults", () => {
|
|
10
|
+
const result = parsePetManifest({
|
|
11
|
+
id: "example-pet",
|
|
12
|
+
name: "Example Pet",
|
|
13
|
+
spritesheet: "spritesheet.webp"
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
assert.equal(result.ok, true);
|
|
17
|
+
assert.deepEqual(result.errors, []);
|
|
18
|
+
assert.equal(result.manifest.id, "example-pet");
|
|
19
|
+
assert.equal(result.manifest.name, "Example Pet");
|
|
20
|
+
assert.equal(result.manifest.spritesheet, "spritesheet.webp");
|
|
21
|
+
assert.equal(result.manifest.cellWidth, 192);
|
|
22
|
+
assert.equal(result.manifest.cellHeight, 208);
|
|
23
|
+
assert.equal(result.manifest.frameDurationMs, DEFAULT_FRAME_DURATION_MS);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("accepts Codex-style displayName and spritesheetPath aliases", () => {
|
|
27
|
+
const result = parsePetManifest({
|
|
28
|
+
id: "toru-pixel",
|
|
29
|
+
displayName: "Toru Pixel",
|
|
30
|
+
description: "A pixel pet",
|
|
31
|
+
spritesheetPath: "spritesheet.webp"
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
assert.equal(result.ok, true);
|
|
35
|
+
assert.equal(result.manifest.id, "toru-pixel");
|
|
36
|
+
assert.equal(result.manifest.name, "Toru Pixel");
|
|
37
|
+
assert.equal(result.manifest.spritesheet, "spritesheet.webp");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("prefers canonical fields over aliases when both are present", () => {
|
|
41
|
+
const result = parsePetManifest({
|
|
42
|
+
id: "p",
|
|
43
|
+
name: "Canonical",
|
|
44
|
+
displayName: "Alias",
|
|
45
|
+
spritesheet: "real.webp",
|
|
46
|
+
spritesheetPath: "alias.webp"
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
assert.equal(result.manifest.name, "Canonical");
|
|
50
|
+
assert.equal(result.manifest.spritesheet, "real.webp");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("rejects manifests missing required fields", () => {
|
|
54
|
+
const result = parsePetManifest({ name: "No Id" });
|
|
55
|
+
|
|
56
|
+
assert.equal(result.ok, false);
|
|
57
|
+
assert.equal(result.manifest, undefined);
|
|
58
|
+
assert.ok(result.errors.some((message) => message.includes("id")));
|
|
59
|
+
assert.ok(result.errors.some((message) => message.includes("spritesheet")));
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("rejects cell dimensions that do not match the Codex atlas", () => {
|
|
63
|
+
const result = parsePetManifest({
|
|
64
|
+
id: "p",
|
|
65
|
+
name: "P",
|
|
66
|
+
spritesheet: "s.webp",
|
|
67
|
+
cellWidth: 200,
|
|
68
|
+
cellHeight: 208
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
assert.equal(result.ok, false);
|
|
72
|
+
assert.ok(result.errors.some((message) => message.includes("cellWidth")));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("normalizes per-action frame duration overrides", () => {
|
|
76
|
+
const result = parsePetManifest({
|
|
77
|
+
id: "p",
|
|
78
|
+
name: "P",
|
|
79
|
+
spritesheet: "s.webp",
|
|
80
|
+
frameDurationMs: 100,
|
|
81
|
+
actionFrameDurations: { idle: 250 }
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
assert.equal(result.ok, true);
|
|
85
|
+
assert.equal(getFrameDurationMs(result.manifest, "idle"), 250);
|
|
86
|
+
assert.equal(getFrameDurationMs(result.manifest, "running"), 100);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("rejects non-object manifests", () => {
|
|
90
|
+
const result = parsePetManifest("not-json");
|
|
91
|
+
assert.equal(result.ok, false);
|
|
92
|
+
assert.ok(result.errors.some((message) => message.includes("object")));
|
|
93
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "../../../test/harness.mjs";
|
|
3
|
+
import {
|
|
4
|
+
validateAtlasDimensions,
|
|
5
|
+
validatePetActions,
|
|
6
|
+
validatePet
|
|
7
|
+
} from "../src/validation.js";
|
|
8
|
+
|
|
9
|
+
const FULL_ACTION_FRAMES = {
|
|
10
|
+
idle: 6,
|
|
11
|
+
"running-right": 8,
|
|
12
|
+
"running-left": 8,
|
|
13
|
+
waving: 4,
|
|
14
|
+
jumping: 5,
|
|
15
|
+
failed: 8,
|
|
16
|
+
waiting: 6,
|
|
17
|
+
running: 6,
|
|
18
|
+
review: 6
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
test("accepts the Codex-compatible 1536x1872 atlas", () => {
|
|
22
|
+
const result = validateAtlasDimensions({ width: 1536, height: 1872 });
|
|
23
|
+
assert.equal(result.ok, true);
|
|
24
|
+
assert.deepEqual(result.errors, []);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("rejects atlases that are not 1536x1872", () => {
|
|
28
|
+
const result = validateAtlasDimensions({ width: 1024, height: 1872 });
|
|
29
|
+
assert.equal(result.ok, false);
|
|
30
|
+
assert.ok(result.errors.some((message) => message.includes("1536")));
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("requires every action row to have non-empty used cells", () => {
|
|
34
|
+
const ok = validatePetActions(FULL_ACTION_FRAMES);
|
|
35
|
+
assert.equal(ok.ok, true);
|
|
36
|
+
|
|
37
|
+
const missing = { ...FULL_ACTION_FRAMES };
|
|
38
|
+
delete missing.review;
|
|
39
|
+
const missingResult = validatePetActions(missing);
|
|
40
|
+
assert.equal(missingResult.ok, false);
|
|
41
|
+
assert.ok(missingResult.errors.some((message) => message.includes("review")));
|
|
42
|
+
|
|
43
|
+
const emptyRow = { ...FULL_ACTION_FRAMES, waving: 0 };
|
|
44
|
+
const emptyResult = validatePetActions(emptyRow);
|
|
45
|
+
assert.equal(emptyResult.ok, false);
|
|
46
|
+
assert.ok(emptyResult.errors.some((message) => message.includes("waving")));
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("combines manifest, dimension, and action checks", () => {
|
|
50
|
+
const result = validatePet({
|
|
51
|
+
manifest: { id: "p", name: "P", spritesheet: "s.webp" },
|
|
52
|
+
dimensions: { width: 1536, height: 1872 },
|
|
53
|
+
actionFrameCounts: FULL_ACTION_FRAMES
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
assert.equal(result.ok, true);
|
|
57
|
+
assert.deepEqual(result.errors, []);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("surfaces all failures from a broken pet", () => {
|
|
61
|
+
const result = validatePet({
|
|
62
|
+
manifest: { name: "P" },
|
|
63
|
+
dimensions: { width: 100, height: 100 },
|
|
64
|
+
actionFrameCounts: { idle: 6 }
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
assert.equal(result.ok, false);
|
|
68
|
+
assert.ok(result.errors.length >= 3);
|
|
69
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { normalizePlatform } from "./platform.js";
|
|
2
|
+
|
|
3
|
+
export function getPlatformCapabilities(options = {}) {
|
|
4
|
+
const platform = normalizePlatform(options.platform);
|
|
5
|
+
const env = options.env ?? process.env;
|
|
6
|
+
|
|
7
|
+
if (platform === "windows") {
|
|
8
|
+
return {
|
|
9
|
+
platform,
|
|
10
|
+
ipcTransport: "named-pipe",
|
|
11
|
+
transparentOverlay: "required",
|
|
12
|
+
terminalAttachment: "best-effort",
|
|
13
|
+
displayManagement: "required",
|
|
14
|
+
fallbackMode: "none"
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (platform === "macos") {
|
|
19
|
+
return {
|
|
20
|
+
platform,
|
|
21
|
+
ipcTransport: "unix-socket",
|
|
22
|
+
transparentOverlay: "required",
|
|
23
|
+
terminalAttachment: "best-effort",
|
|
24
|
+
displayManagement: "required",
|
|
25
|
+
fallbackMode: "none"
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (platform === "linux") {
|
|
30
|
+
const isWayland = Boolean(env.WAYLAND_DISPLAY);
|
|
31
|
+
return {
|
|
32
|
+
platform,
|
|
33
|
+
ipcTransport: "unix-socket",
|
|
34
|
+
transparentOverlay: isWayland ? "best-effort" : "required",
|
|
35
|
+
terminalAttachment: isWayland ? "fallback" : "best-effort",
|
|
36
|
+
displayManagement: "required",
|
|
37
|
+
fallbackMode: isWayland ? "wayland-cluster" : "none"
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
platform,
|
|
43
|
+
ipcTransport: "unsupported",
|
|
44
|
+
transparentOverlay: "fallback",
|
|
45
|
+
terminalAttachment: "fallback",
|
|
46
|
+
displayManagement: "fallback",
|
|
47
|
+
fallbackMode: "normal-window"
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { normalizePlatform } from "./platform.js";
|
|
2
|
+
|
|
3
|
+
export function getDefaultPaths(options = {}) {
|
|
4
|
+
const platform = normalizePlatform(options.platform);
|
|
5
|
+
const env = options.env ?? process.env;
|
|
6
|
+
const homeDir = options.homeDir ?? env.HOME ?? env.USERPROFILE;
|
|
7
|
+
|
|
8
|
+
if (!homeDir) {
|
|
9
|
+
throw new Error("homeDir is required when no HOME or USERPROFILE environment variable is available");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (platform === "windows") {
|
|
13
|
+
return getWindowsPaths(env, homeDir);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (platform === "macos" || platform === "linux") {
|
|
17
|
+
return getUnixPaths(homeDir);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return getUnsupportedPaths(homeDir);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getWindowsPaths(env, homeDir) {
|
|
24
|
+
const localAppData = env.LOCALAPPDATA ?? joinWindows(homeDir, "AppData", "Local");
|
|
25
|
+
const appData = env.APPDATA ?? joinWindows(homeDir, "AppData", "Roaming");
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
platform: "windows",
|
|
29
|
+
ipcEndpoint: "\\\\.\\pipe\\haya-petd",
|
|
30
|
+
statePath: joinWindows(localAppData, "haya-pet", "state.json"),
|
|
31
|
+
configPath: joinWindows(appData, "haya-pet", "config.json"),
|
|
32
|
+
logDir: joinWindows(localAppData, "haya-pet", "logs"),
|
|
33
|
+
petSearchPaths: [
|
|
34
|
+
joinWindows(homeDir, ".codex", "pets"),
|
|
35
|
+
joinWindows(localAppData, "haya-pet", "pets")
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getUnixPaths(homeDir) {
|
|
41
|
+
return {
|
|
42
|
+
platform: "unix",
|
|
43
|
+
ipcEndpoint: joinUnix(homeDir, ".haya-pet", "haya-petd.sock"),
|
|
44
|
+
statePath: joinUnix(homeDir, ".haya-pet", "state.json"),
|
|
45
|
+
configPath: joinUnix(homeDir, ".haya-pet", "config.json"),
|
|
46
|
+
logDir: joinUnix(homeDir, ".haya-pet", "logs"),
|
|
47
|
+
petSearchPaths: [
|
|
48
|
+
joinUnix(homeDir, ".codex", "pets"),
|
|
49
|
+
joinUnix(homeDir, ".haya-pet", "pets")
|
|
50
|
+
]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getUnsupportedPaths(homeDir) {
|
|
55
|
+
return {
|
|
56
|
+
platform: "unsupported",
|
|
57
|
+
ipcEndpoint: joinUnix(homeDir, ".haya-pet", "haya-petd.sock"),
|
|
58
|
+
statePath: joinUnix(homeDir, ".haya-pet", "state.json"),
|
|
59
|
+
configPath: joinUnix(homeDir, ".haya-pet", "config.json"),
|
|
60
|
+
logDir: joinUnix(homeDir, ".haya-pet", "logs"),
|
|
61
|
+
petSearchPaths: [
|
|
62
|
+
joinUnix(homeDir, ".codex", "pets"),
|
|
63
|
+
joinUnix(homeDir, ".haya-pet", "pets")
|
|
64
|
+
]
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function joinWindows(...parts) {
|
|
69
|
+
return parts.filter(Boolean).join("\\").replace(/\\+/g, "\\");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function joinUnix(...parts) {
|
|
73
|
+
const [first, ...rest] = parts.filter(Boolean);
|
|
74
|
+
return [first.replace(/\/+$/g, ""), ...rest.map((part) => part.replace(/^\/+|\/+$/g, ""))].join("/");
|
|
75
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function normalizePlatform(platform = process.platform) {
|
|
2
|
+
if (platform === "win32" || platform === "windows") {
|
|
3
|
+
return "windows";
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (platform === "darwin" || platform === "macos") {
|
|
7
|
+
return "macos";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (platform === "linux") {
|
|
11
|
+
return "linux";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return "unsupported";
|
|
15
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { test } from "../../../test/harness.mjs";
|
|
3
|
+
import { getPlatformCapabilities } from "../src/capabilities.js";
|
|
4
|
+
import { getDefaultPaths } from "../src/paths.js";
|
|
5
|
+
import { normalizePlatform } from "../src/platform.js";
|
|
6
|
+
|
|
7
|
+
test("normalizes supported and unsupported platforms", () => {
|
|
8
|
+
assert.equal(normalizePlatform("win32"), "windows");
|
|
9
|
+
assert.equal(normalizePlatform("darwin"), "macos");
|
|
10
|
+
assert.equal(normalizePlatform("linux"), "linux");
|
|
11
|
+
assert.equal(normalizePlatform("freebsd"), "unsupported");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("returns Windows local app paths and named pipe endpoint", () => {
|
|
15
|
+
const paths = getDefaultPaths({
|
|
16
|
+
platform: "windows",
|
|
17
|
+
env: {
|
|
18
|
+
LOCALAPPDATA: "C:\\Users\\A\\AppData\\Local",
|
|
19
|
+
APPDATA: "C:\\Users\\A\\AppData\\Roaming",
|
|
20
|
+
USERPROFILE: "C:\\Users\\A"
|
|
21
|
+
},
|
|
22
|
+
homeDir: "C:\\Users\\A"
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
assert.equal(paths.ipcEndpoint, "\\\\.\\pipe\\haya-petd");
|
|
26
|
+
assert.equal(paths.statePath, "C:\\Users\\A\\AppData\\Local\\haya-pet\\state.json");
|
|
27
|
+
assert.equal(paths.configPath, "C:\\Users\\A\\AppData\\Roaming\\haya-pet\\config.json");
|
|
28
|
+
assert.deepEqual(paths.petSearchPaths, [
|
|
29
|
+
"C:\\Users\\A\\.codex\\pets",
|
|
30
|
+
"C:\\Users\\A\\AppData\\Local\\haya-pet\\pets"
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("returns macOS local app paths and Unix socket endpoint", () => {
|
|
35
|
+
const paths = getDefaultPaths({
|
|
36
|
+
platform: "macos",
|
|
37
|
+
env: {},
|
|
38
|
+
homeDir: "/Users/a"
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
assert.equal(paths.ipcEndpoint, "/Users/a/.haya-pet/haya-petd.sock");
|
|
42
|
+
assert.equal(paths.statePath, "/Users/a/.haya-pet/state.json");
|
|
43
|
+
assert.equal(paths.configPath, "/Users/a/.haya-pet/config.json");
|
|
44
|
+
assert.deepEqual(paths.petSearchPaths, [
|
|
45
|
+
"/Users/a/.codex/pets",
|
|
46
|
+
"/Users/a/.haya-pet/pets"
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("returns Linux local app paths and Unix socket endpoint", () => {
|
|
51
|
+
const paths = getDefaultPaths({
|
|
52
|
+
platform: "linux",
|
|
53
|
+
env: {},
|
|
54
|
+
homeDir: "/home/a"
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
assert.equal(paths.ipcEndpoint, "/home/a/.haya-pet/haya-petd.sock");
|
|
58
|
+
assert.equal(paths.statePath, "/home/a/.haya-pet/state.json");
|
|
59
|
+
assert.equal(paths.configPath, "/home/a/.haya-pet/config.json");
|
|
60
|
+
assert.deepEqual(paths.petSearchPaths, [
|
|
61
|
+
"/home/a/.codex/pets",
|
|
62
|
+
"/home/a/.haya-pet/pets"
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("reports platform capabilities and Wayland fallbacks", () => {
|
|
67
|
+
assert.deepEqual(getPlatformCapabilities({ platform: "windows", env: {} }), {
|
|
68
|
+
platform: "windows",
|
|
69
|
+
ipcTransport: "named-pipe",
|
|
70
|
+
transparentOverlay: "required",
|
|
71
|
+
terminalAttachment: "best-effort",
|
|
72
|
+
displayManagement: "required",
|
|
73
|
+
fallbackMode: "none"
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
assert.equal(
|
|
77
|
+
getPlatformCapabilities({ platform: "linux", env: { WAYLAND_DISPLAY: "wayland-0" } }).terminalAttachment,
|
|
78
|
+
"fallback"
|
|
79
|
+
);
|
|
80
|
+
assert.equal(
|
|
81
|
+
getPlatformCapabilities({ platform: "linux", env: { WAYLAND_DISPLAY: "wayland-0" } }).transparentOverlay,
|
|
82
|
+
"best-effort"
|
|
83
|
+
);
|
|
84
|
+
});
|