Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Keep it platform-focused, typed, and useful for both humans and agents.

- Use Effect at the runtime boundary and for reusable effectful operations. Keep
errors schema-backed and render them without stack traces in CLI output.
- Use `effect/unstable/cli` `Argument` and `Flag` primitives for command
- Use `effect/cli` `Argument` and `Flag` primitives for command
grammar before adding custom argv parsing.
- Keep CLI wiring thin: parse/dispatch commands, then call named Roku helpers.
- Keep `src/roku.ts` as a public compatibility barrel. Put implementation in
Expand Down Expand Up @@ -71,7 +71,7 @@ touches, and search `node_modules/effect/src` for anything it does not cover.
- CLI tests default to in-process entry points (`mainEffect` from `src/cli.ts`
or the command effects) so V8 coverage attributes them. Spawn
`dist/rokit.mjs` only when the process boundary itself is under test; those
runs are invisible to coverage on vitest 4 (see `vite.config.ts`).
runs are not attributed to coverage (see `vite.config.ts`).
- `ROKU_DEV_TARGET` and `ROKU_DEV_PASSWORD` are optional fallback aliases, not
the primary public contract; the env contract is in [README.md](README.md#quick-start).
- Avoid sleeps in generic commands. App repos can add meaningful wait/assert
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@
"verify": "vp check . && skillcheck lint && tsc --noEmit && vp pack src/index.ts src/rokit.ts --dts && node dist/rokit.mjs --version && node dist/rokit.mjs --help >/dev/null && vp test run --coverage && npm pack --dry-run"
},
"dependencies": {
"@effect/platform-node": "4.0.0-beta.107",
"@effect/platform-node-shared": "4.0.0-beta.107",
"effect": "4.0.0-beta.107",
"@effect/platform-node": "4.0.0-rc.118",
"@effect/platform-node-shared": "4.0.0-rc.118",
"effect": "4.0.0-rc.118",
"jszip": "^3.10.1",
"pngjs": "7.0.0",
"roku-deploy": "3.18.2"
},
"devDependencies": {
"@effect/vitest": "4.0.0-beta.107",
"@effect/vitest": "4.0.0-rc.118",
"@types/node": "^26.1.2",
"@types/pngjs": "6.0.5",
"@uinaf/skillcheck": "0.4.0",
Expand Down
1,344 changes: 593 additions & 751 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

28 changes: 19 additions & 9 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ packages:
- "."

overrides:
"@effect/platform-node-shared": 4.0.0-beta.107
"@effect/platform-node-shared": 4.0.0-rc.118
vite: "catalog:"
vitest: "catalog:"

Expand All @@ -18,16 +18,26 @@ allowBuilds:
sharp: false
minimumReleaseAgeExclude:
# Exact versions or `pkg@v1 || v2` unions only; `pkg@^range` is rejected by design.
- "@effect/platform-node-shared@4.0.0-beta.107"
- "@effect/platform-node@4.0.0-beta.107"
- "@effect/vitest@4.0.0-beta.107"
- "effect@4.0.0-beta.107"
- "@effect/platform-node-shared@4.0.0-rc.118"
- "@effect/platform-node@4.0.0-rc.118"
- "@effect/vitest@4.0.0-rc.118"
- "effect@4.0.0-rc.118"
- "@uinaf/skillcheck@0.4.0"
- "@voidzero-dev/vite-plus-core@1.0.0"
- "@voidzero-dev/vite-plus-darwin-arm64@1.0.0"
- "@voidzero-dev/vite-plus-darwin-x64@1.0.0"
- "@voidzero-dev/vite-plus-linux-arm64-gnu@1.0.0"
- "@voidzero-dev/vite-plus-linux-arm64-musl@1.0.0"
- "@voidzero-dev/vite-plus-linux-x64-gnu@1.0.0"
- "@voidzero-dev/vite-plus-linux-x64-musl@1.0.0"
- "@voidzero-dev/vite-plus-win32-arm64-msvc@1.0.0"
- "@voidzero-dev/vite-plus-win32-x64-msvc@1.0.0"
- vite-plus@1.0.0
catalog:
"@vitest/coverage-v8": 4.1.10
vite: npm:@voidzero-dev/vite-plus-core@0.2.9
vitest: 4.1.10
vite-plus: 0.2.9
"@vitest/coverage-v8": 5.0.1
vite: npm:@voidzero-dev/vite-plus-core@1.0.0
vitest: 5.0.1
vite-plus: 1.0.0
peerDependencyRules:
allowAny:
- vite
Expand Down
22 changes: 11 additions & 11 deletions src/cli-argument.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,44 @@
import { Option } from "effect";
import { Argument, Flag } from "effect/unstable/cli";
import { Argument, Flag } from "effect/cli";
import type { DescribedField } from "./cli-types.js";
import { validateRemoteKey } from "./ecp.js";

export const stringArgument = (name: string, description: string) =>
Argument.string(name).pipe(Argument.withDescription(description));
Argument.String(name).pipe(Argument.withDescription(description));

export const stringArgumentField = (field: DescribedField) =>
stringArgument(field.name, field.description);

export const pathArgument = (name: string, description: string) =>
Argument.path(name).pipe(Argument.withDescription(description));
Argument.Path(name).pipe(Argument.withDescription(description));

export const pathArgumentField = (field: DescribedField) =>
pathArgument(field.name, field.description);

export const fileArgument = (
name: string,
description: string,
options: Parameters<typeof Argument.file>[1],
) => Argument.file(name, options).pipe(Argument.withDescription(description));
options: Parameters<typeof Argument.File>[1],
) => Argument.File(name, options).pipe(Argument.withDescription(description));

export const fileArgumentField = (
field: DescribedField,
options: Parameters<typeof Argument.file>[1],
options: Parameters<typeof Argument.File>[1],
) => fileArgument(field.name, field.description, options);

export const choiceArgument = <const Choices extends readonly [string, ...string[]]>(
name: string,
choices: Choices,
description: string,
) => Argument.choice(name, choices).pipe(Argument.withDescription(description));
) => Argument.Literals(name, choices).pipe(Argument.withDescription(description));

export const choiceArgumentField = <const Choices extends readonly [string, ...string[]]>(
field: DescribedField,
choices: Choices,
) => choiceArgument(field.name, choices, field.description);

export const positiveIntegerFlag = (name: string, label: string, description: string) =>
Flag.integer(name).pipe(
Flag.Int(name).pipe(
Flag.filter(
(value) => value > 0,
(value) => `Invalid ${label}: ${value}`,
Expand All @@ -50,7 +50,7 @@ export const positiveIntegerFlagField = (field: DescribedField, label = field.na
positiveIntegerFlag(field.name, label, field.description);

export const nonNegativeIntegerFlag = (name: string, label: string, description: string) =>
Flag.integer(name).pipe(
Flag.Int(name).pipe(
Flag.filter(
(value) => value >= 0,
(value) => `Invalid ${label}: ${value}`,
Expand All @@ -62,7 +62,7 @@ export const nonNegativeIntegerFlagField = (field: DescribedField, label = field
nonNegativeIntegerFlag(field.name, label, field.description);

export const stringFlagField = (field: DescribedField) =>
Flag.string(field.name).pipe(Flag.withDescription(field.description));
Flag.String(field.name).pipe(Flag.withDescription(field.description));

export const remoteKeyArgument = (field: DescribedField) =>
stringArgumentField(field).pipe(
Expand All @@ -72,7 +72,7 @@ export const remoteKeyArgument = (field: DescribedField) =>
export const launchParamsFlag = (field: DescribedField) =>
// Roku query params can contain "=" in the value; Effect's keyValuePair primitive
// rejects that, so this parser splits only on the first separator.
Flag.string(field.name).pipe(
Flag.String(field.name).pipe(
Flag.withMetavar("key=value"),
Flag.withDescription(field.description),
Flag.atMost(Number.MAX_SAFE_INTEGER),
Expand Down
6 changes: 3 additions & 3 deletions src/cli-artifact-commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Effect } from "effect";
import { Argument, Command as EffectCommand, Flag } from "effect/unstable/cli";
import { Argument, Command as EffectCommand, Flag } from "effect/cli";
import {
fileArgumentField,
optionToUndefined,
Expand All @@ -20,7 +20,7 @@ export const artifactCommands = (capture: CommandCapture) => [
),
strictCommand("proof", {
outputDir: pathArgumentField(commandParameter("proof", "output-dir")),
screenshot: Flag.boolean("screenshot").pipe(
screenshot: Flag.Boolean("screenshot").pipe(
Flag.withDescription(commandParameter("proof", "screenshot").description),
),
}).pipe(
Expand All @@ -37,7 +37,7 @@ export const artifactCommands = (capture: CommandCapture) => [
outputPath: stringArgumentField(commandParameter("package", "zip-path")).pipe(
Argument.optional,
),
out: Flag.string("out").pipe(
out: Flag.String("out").pipe(
Flag.withMetavar("zip-path"),
Flag.withDescription(commandParameter("package", "zip-path").description),
Flag.optional,
Expand Down
4 changes: 2 additions & 2 deletions src/cli-command-shared.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Effect } from "effect";
import { Argument, Command as EffectCommand } from "effect/unstable/cli";
import { Argument, Command as EffectCommand } from "effect/cli";
import type { Command } from "./cli-types.js";
import type { RokitGlobalFlagContext } from "./cli-options.js";

Expand All @@ -18,7 +18,7 @@ export const strictCommand = <
...config,
// Effect CLI leaves surplus positional arguments unconsumed by default.
// Keep command definitions idiomatic while making rokit reject typos.
extra: Argument.string("extra").pipe(
extra: Argument.String("extra").pipe(
Argument.variadic(),
Argument.filter(
(extra) => extra.length === 0,
Expand Down
2 changes: 1 addition & 1 deletion src/cli-command.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NodeServices } from "@effect/platform-node";
import { Console, Effect } from "effect";
import { CliError, CliOutput, Command as EffectCommand } from "effect/unstable/cli";
import { CliError, CliOutput, Command as EffectCommand } from "effect/cli";
import PackageJson from "../package.json" with { type: "json" };
import type { Command, ParsedCli } from "./cli-types.js";
import { artifactCommands } from "./cli-artifact-commands.js";
Expand Down
4 changes: 2 additions & 2 deletions src/cli-control-commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Effect } from "effect";
import { Argument, Command as EffectCommand, Flag } from "effect/unstable/cli";
import { Argument, Command as EffectCommand, Flag } from "effect/cli";
import {
launchParamsFlag,
nonNegativeIntegerFlagField,
Expand Down Expand Up @@ -40,7 +40,7 @@ export const controlCommands = (capture: CommandCapture) => [
Flag.optional,
),
untilNodeName: stringFlagField(commandParameter("press", "until-node")).pipe(Flag.optional),
untilNodeState: Flag.choice("until-state", nodeConditionStates).pipe(
untilNodeState: Flag.Literals("until-state", nodeConditionStates).pipe(
Flag.withDescription(commandParameter("press", "until-state").description),
Flag.optional,
),
Expand Down
2 changes: 1 addition & 1 deletion src/cli-debug-commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Effect } from "effect";
import { Argument, Command as EffectCommand, Flag } from "effect/unstable/cli";
import { Argument, Command as EffectCommand, Flag } from "effect/cli";
import {
pathArgumentField,
positiveIntegerFlagField,
Expand Down
2 changes: 1 addition & 1 deletion src/cli-meta-commands.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Argument, Command as EffectCommand } from "effect/unstable/cli";
import { Argument, Command as EffectCommand } from "effect/cli";
import { optionToUndefined, stringArgumentField } from "./cli-argument.js";
import { commandDescription, commandParameter } from "./cli-command-metadata.js";
import type { CommandCapture } from "./cli-command-shared.js";
Expand Down
2 changes: 1 addition & 1 deletion src/cli-observation-commands.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Effect } from "effect";
import { Argument, Command as EffectCommand, Flag } from "effect/unstable/cli";
import { Argument, Command as EffectCommand, Flag } from "effect/cli";
import type { Command } from "./cli-types.js";
import {
choiceArgumentField,
Expand Down
34 changes: 17 additions & 17 deletions src/cli-options.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
import { Effect, Option } from "effect";
import { Flag, GlobalFlag } from "effect/unstable/cli";
import { Flag, GlobalFlag } from "effect/cli";
import { globalOption } from "./cli-command-metadata.js";
import type { CliOptions, OutputMode } from "./cli-types.js";

export type RokitGlobalFlagContext =
| "effect/unstable/cli/GlobalFlag/dry-run"
| "effect/unstable/cli/GlobalFlag/fields"
| "effect/unstable/cli/GlobalFlag/input-json"
| "effect/unstable/cli/GlobalFlag/json"
| "effect/unstable/cli/GlobalFlag/output";

export const DryRunGlobal = GlobalFlag.setting("dry-run")({
flag: Flag.boolean("dry-run").pipe(
| "effect/cli/GlobalFlag/dry-run"
| "effect/cli/GlobalFlag/fields"
| "effect/cli/GlobalFlag/input-json"
| "effect/cli/GlobalFlag/json"
| "effect/cli/GlobalFlag/output";

export const DryRunGlobal = GlobalFlag.Setting("dry-run")({
flag: Flag.Boolean("dry-run").pipe(
Flag.withDefault(false),
Flag.withDescription(globalOption("dry-run").description),
),
});

export const FieldsGlobal = GlobalFlag.setting("fields")({
flag: Flag.string("fields").pipe(
export const FieldsGlobal = GlobalFlag.Setting("fields")({
flag: Flag.String("fields").pipe(
Flag.optional,
Flag.withDescription(globalOption("fields").description),
),
});

export const InputJsonGlobal = GlobalFlag.setting("input-json")({
flag: Flag.string("input-json").pipe(
export const InputJsonGlobal = GlobalFlag.Setting("input-json")({
flag: Flag.String("input-json").pipe(
Flag.optional,
Flag.withDescription(globalOption("input-json").description),
),
});

export const JsonGlobal = GlobalFlag.setting("json")({
flag: Flag.boolean("json").pipe(
export const JsonGlobal = GlobalFlag.Setting("json")({
flag: Flag.Boolean("json").pipe(
Flag.withDefault(false),
Flag.withDescription(globalOption("json").description),
),
});

export const OutputGlobal = GlobalFlag.setting("output")({
flag: Flag.choice("output", ["json", "text"]).pipe(
export const OutputGlobal = GlobalFlag.Setting("output")({
flag: Flag.Literals("output", ["json", "text"]).pipe(
Flag.optional,
Flag.withDescription(globalOption("output").description),
),
Expand Down
9 changes: 4 additions & 5 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ export default defineConfig({
test: {
coverage: {
// Coverage blind spot: the boot test spawns the packaged dist/rokit.mjs
// to prove the real binary starts; V8 coverage cannot attribute
// subprocess execution on vitest 4, so the bin shim src/rokit.ts is
// excluded by design. All other CLI tests run the same entry in-process
// (mainEffect and the command effects). When vite-plus ships vitest 5,
// coverage.autoAttachSubprocess can close the remaining gap.
// to prove the real binary starts; that subprocess is not attributed to
// coverage, so the bin shim src/rokit.ts is excluded by design. All other
// CLI tests run the same entry in-process (mainEffect and the command
// effects). Vitest 5 coverage.autoAttachSubprocess can close the gap.
exclude: ["src/**/*.d.ts", "src/rokit.ts"],
include: ["src/**/*.ts"],
provider: "v8",
Expand Down