Skip to content
Merged

Next #35

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
64 changes: 64 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,68 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v1.2.0] - 2026-05-19 - The one with the shader and particles playground

### Added

- **Shader Graph** — node-based visual GLSL editor for authoring Love2D pixel and vertex shaders without writing code.
- 40+ built-in nodes across 9 categories: Input, Math, Vector, Color, UV, Noise, Effect, Vertex, and Output.
- Input nodes: Texture Color, Texture Coords, Screen Coords, Vertex Color, Time, Resolution, Float, Vec2, Vec3, Vec4 constants.
- Math nodes: Add, Subtract, Multiply, Divide, Power, Sqrt, Abs, Clamp, Lerp, Step, Smoothstep, Fract, Floor, Ceil, Mod, Min, Max, Sign, Sin, Cos, Atan2.
- Vector nodes: Combine2, Combine3, SplitVec2, SplitVec3, Dot, Cross, Normalize, Length, MatVecMul, TransformMatrix.
- Color nodes: Desaturate, OneMinus, HueShift, InvertColor, Contrast, PosterizeColor, MultiplyColor.
- UV nodes: TilingOffset, RotateUV, TwirlUV, PolarCoordinates.
- Noise nodes: SimpleNoise, Ripple, VoronoiCells, Checkerboard.
- Vertex nodes: VertexPosition, VertexWave2D with a dedicated VertexOutput node for vertex shader authoring.
- Type-safe port connections — incompatible types are rejected; handle colors reflect the GLSL type.
- GLSL code generation with pixel and vertex shader output, extern declarations, and shared noise helpers.
- Live GLSL preview panel with syntax highlighting, copy to clipboard, line numbers, and light/dark theme support.
- Validate against the running game via the `shader-graph` Lua plugin (`compile-shader` action).
- Apply generated shader directly to a Particle System Playground composite target.
- Undo/redo history (up to 100 snapshots), Delete/Backspace to remove selected nodes or edges.
- Import/export graph as `.feathershgh` JSON files.
- Bundled preset graphs (chromatic aberration, pixelation, grayscale, vignette, wave distortion, and more).
- Enable/disable toggle integrated with the plugin control system; empty state when the plugin is not active.
- New `shader-graph` Lua plugin for in-game GLSL compilation and validation.

- **Particle System Playground** — live editor for Love2D composite particle systems with real-time in-game preview.
- Side-by-side emitter list and full properties panel covering all 30+ Love2D `ParticleSystem` parameters.
- Visual property editors: color gradient with alpha curve lane, size curve with draggable Catmull-Rom spline, direction/spread gizmo, rotation/spin gizmo, circular force gizmo, linear acceleration plane, damping range editor, and texture offset gizmo.
- Range pair fields for all min/max properties (speed, spin, rotation, acceleration, damping) with a connecting bar visual.
- 30 built-in motion presets: Explosion, Whirlpool, Tornado, Wind Drift, Smoke Rise, Gravity Fall, Orbit, Shockwave, Fountain, Fire Sparks, Ember Float, Rain, Snow Drift, Magic Swirl, Portal Inhale, Portal Burst, Spiral Up, Spiral Down, Jet Thruster, Muzzle Flash, Blood Spray, Debris Arc, Dust Puff, Steam Vent, Bubbles, Water Splash, Leaf Gust, Sparks Shower, Energy Beam, Implosion — each with an intensify variant.
- Texture importer with base64 transport and live preview inside the editor.
- Shader editor panel with preset shader library and live apply to game.
- Composite and system selector for multi-emitter setups.
- Export panel for saving the full composite configuration.
- New `particle-system-playground` Lua plugin with `draw` and `filesystem` capabilities.

- **Session page** — dedicated `/session` route showing an overview of the active game session.
- Session card: name, device ID, session ID, insecure connection warning.
- Environment card: OS, architecture, CPU cores, LÖVE version (when reported by the runtime), Feather runtime version, and API version.
- Plugins section: all plugins reported by the session with name, version, and enabled/disabled/incompatible badge.
- Packages section: reads `feather.lock.json` from the project root via Tauri file API; shows package name, version, and trust level badge (`verified` / `known` / `experimental`). Gracefully unavailable for file sessions, web mode, and remote machines.

- **`feather init` capability inference** — when installing plugins in `auto` or `manual` mode, the generated `feather.config.lua` automatically includes the `capabilities` required by the selected plugins (e.g. installing `console` adds `filesystem`).
- New `--allow-insecure-connection` flag for non-interactive `--yes` flows: `__DANGEROUS_INSECURE_CONNECTION__` is no longer written by default; users must opt in explicitly.

- **Lua config parser** — `feather run` now correctly strips Lua comments before parsing `feather.config.lua`, so commented-out example options no longer interfere with loaded values.

- **`feather run` shim** — CLI-only projects now get an auto-shim that drives `DEBUGGER:update(dt)` without requiring changes to `main.lua`.

### Changed

- `feather doctor` downgrades missing `Desktop App ID` from `fail` to `warn` when `__DANGEROUS_INSECURE_CONNECTION__` is present, reflecting that it is an intentional development override rather than a misconfiguration.

### Fixed

- `feather run` no longer errors on CLI-only projects that have no embedded Feather runtime directory.

### Tests

- `init`: added tests for `--yes` without `--allow-insecure-connection` (config stays clean, doctor fails on appId), `--yes --allow-insecure-connection` (flag written, doctor downgrades to warn), plugin capability propagation into generated config, and interactive mode capability inference.
- `doctor`: added test verifying the missing appId warning when the insecure dev override is enabled.
- `run`: added tests for the CLI-only shim update hook and comment-stripping in the Lua config parser.

## [v1.1.1] - 2026-05-17 - The one with better defaults

### Changes
Expand Down Expand Up @@ -430,6 +492,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- LuaRocks package.
- GitHub Actions CI.

[v1.2.0]: https://github.com/Kyonru/feather/compare/v1.1.1...v1.2.0
[v1.1.1]: https://github.com/Kyonru/feather/compare/v1.1.0...v1.1.1
[v1.1.0]: https://github.com/Kyonru/feather/compare/v1.0.1...v1.1.0
[v1.0.1]: https://github.com/Kyonru/feather/compare/v1.0.0...v1.0.1
[v1.0.0]: https://github.com/Kyonru/feather/compare/v0.10.0...v1.0.0
Expand Down
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kyonru/feather",
"version": "1.1.1",
"version": "1.2.0",
"description": "CLI for Feather — run and debug Love2D games without touching your game code",
"license": "EPL-2.0",
"keywords": [
Expand Down
5 changes: 3 additions & 2 deletions cli/src/commands/doctor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,12 +461,13 @@ export async function doctorCommand(gamePath?: string, opts: DoctorOptions = {})

const appId = typeof config?.appId === 'string' ? config.appId.trim() : '';
appIdMissing = mode !== 'disk' && !appId;
const appIdSeverity = appIdMissing ? (insecureConnection ? productionSeverity(true) : 'fail') : 'pass';
add(
checks,
'Safety',
'Desktop App ID',
appIdMissing ? 'fail' : 'pass',
appIdMissing ? 'missing' : mode === 'disk' ? 'not needed for disk mode' : 'configured',
appIdSeverity,
appIdMissing && insecureConnection ? 'missing; insecure development override enabled' : appIdMissing ? 'missing' : mode === 'disk' ? 'not needed for disk mode' : 'configured',
appIdMissing ? 'Set appId in feather.config.lua before shipping socket/network builds.' : undefined,
);

Expand Down
14 changes: 13 additions & 1 deletion cli/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '../lib/install.js';
import { configTemplate, luaKey, luaValue } from '../lib/config.js';
import { chooseInitMode, type InitMode, type InitSetup } from '../ui/init/index.js';
import { mergeCapabilities } from '../ui/init/config.js';
import { pluginCatalog } from '../generated/plugin-catalog.js';
import { resolveLocalLuaRoot } from '../lib/paths.js';
import { fail } from '../lib/command.js';
Expand All @@ -27,10 +28,19 @@ export interface InitOptions {
installDir?: string;
yes?: boolean;
mode?: InitMode;
allowInsecureConnection?: boolean;
}

const knownPlugins = pluginCatalog.map((plugin) => plugin.id);

function addPluginCapabilities(config: Record<string, unknown>, pluginIds: Iterable<string>): void {
const current = config.capabilities as string[] | 'all' | undefined;
const merged = mergeCapabilities(current, pluginIds);
if (merged && merged !== 'all') {
config.capabilities = merged;
}
}

const toLocalName = (id: string) =>
id
.split(/[-.]/)
Expand Down Expand Up @@ -98,7 +108,7 @@ export async function initCommand(dir: string, opts: InitOptions): Promise<void>
branch: opts.branch ?? 'main',
installDir: opts.installDir ?? 'feather',
installPlugins: opts.noPlugins ? false : true,
config: {},
config: opts.allowInsecureConnection ? { __DANGEROUS_INSECURE_CONNECTION__: true } : {},
exclude: [],
};
const mode = setup.mode;
Expand Down Expand Up @@ -206,6 +216,7 @@ export async function initCommand(dir: string, opts: InitOptions): Promise<void>
}

if (mode === 'auto') {
addPluginCapabilities(setup.config, installedPluginIds);
const patched = patchMainLua(mainPath, installDir);
if (patched) {
printLine(`${icon.success} Patched main.lua with feather.auto require`);
Expand All @@ -219,6 +230,7 @@ export async function initCommand(dir: string, opts: InitOptions): Promise<void>
: pluginsDisabled
? []
: (opts.plugins ?? knownPlugins).filter((id) => !setup.exclude.includes(id));
addPluginCapabilities(setup.config, pluginIds);
const created = writeManualDebugger(target, setup.config, pluginIds, installDir);
const patched = patchMainLuaForManual(mainPath);

Expand Down
23 changes: 23 additions & 0 deletions cli/src/generated/plugin-catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,19 @@ export const pluginCatalog = [
"optIn": false,
"disabled": true
},
{
"id": "particle-system-playground",
"sourceDir": "particle-system-playground",
"name": "Particles Playground",
"description": "Author composite particle effects with live in-game preview",
"version": "1.0.0",
"capabilities": [
"draw",
"filesystem"
],
"optIn": false,
"disabled": false
},
{
"id": "physics-debug",
"sourceDir": "physics-debug",
Expand Down Expand Up @@ -248,6 +261,16 @@ export const pluginCatalog = [
"optIn": false,
"disabled": true
},
{
"id": "shader-graph",
"sourceDir": "shader-graph",
"name": "Shader Graph",
"description": "Validate GLSL shaders authored in the Feather shader graph editor",
"version": "1.0.0",
"capabilities": [],
"optIn": false,
"disabled": false
},
{
"id": "time-travel",
"sourceDir": "time-travel",
Expand Down
2 changes: 2 additions & 0 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ program
.option('--plugins <ids>', 'Comma-separated list of plugins to install')
.option('--mode <mode>', 'Setup mode: cli, auto, or manual', parseInitMode)
.option('-y, --yes', 'Skip confirmation prompts')
.option('--allow-insecure-connection', 'Set __DANGEROUS_INSECURE_CONNECTION__ in feather.config.lua (required with --yes if appId is not configured)')
.action((dir: string | undefined, opts) => runCliAction(() => initCommand(dir ?? '.', {
branch: opts.branch as string,
remote: opts.remote as boolean | undefined,
Expand All @@ -116,6 +117,7 @@ program
: undefined,
mode: opts.mode as InitMode | undefined,
yes: opts.yes as boolean,
allowInsecureConnection: opts.allowInsecureConnection as boolean | undefined,
})));

program
Expand Down
2 changes: 1 addition & 1 deletion cli/src/lib/build/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export function readBuildConfig(projectDir: string, configPath?: string): Feathe
}
return parsed as FeatherBuildConfig;
} catch (err) {
throw new Error(`Invalid feather.build.json: ${(err as Error).message}`);
throw new Error(`Invalid feather.build.json: ${(err as Error).message}`, { cause: err });
}
}

Expand Down
49 changes: 49 additions & 0 deletions cli/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,58 @@ export interface FeatherConfig {
[key: string]: unknown;
}

function stripLuaComments(src: string): string {
let out = "";
let i = 0;
let quote: '"' | "'" | null = null;

while (i < src.length) {
const ch = src[i];
const next = src[i + 1];

if (quote) {
out += ch;
if (ch === "\\" && next) {
out += next;
i += 2;
continue;
}
if (ch === quote) quote = null;
i += 1;
continue;
}

if (ch === '"' || ch === "'") {
quote = ch;
out += ch;
i += 1;
continue;
}

if (ch === "-" && next === "-") {
if (src[i + 2] === "[" && src[i + 3] === "[") {
const end = src.indexOf("]]", i + 4);
if (end === -1) break;
i = end + 2;
continue;
}

while (i < src.length && src[i] !== "\n") i += 1;
continue;
}

out += ch;
i += 1;
}

return out;
}

// Minimal Lua table parser for simple `return { key = value, ... }` configs.
// Handles strings, numbers, booleans, and flat string arrays.
function parseLuaTable(src: string): Record<string, unknown> {
src = stripLuaComments(src);

// Strip the outer `return { ... }` wrapper
const match = src.match(/return\s*\{([\s\S]*)\}/);
if (!match) return {};
Expand Down
23 changes: 23 additions & 0 deletions cli/src/lib/shim.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,29 @@ if gamePath then
end
chunk()
end

-- CLI mode should not require game code to call DEBUGGER:update(dt).
-- Wrap after loading the game so we preserve the user's love.update callback.
-- If the game already calls DEBUGGER:update(dt), avoid a second update in the same frame.
if DEBUGGER and type(DEBUGGER.update) == "function" and not DEBUGGER.__cliAutoUpdateInstalled then
DEBUGGER.__cliAutoUpdateInstalled = true
local featherUpdate = DEBUGGER.update
DEBUGGER.update = function(self, dt)
self.__cliAutoUpdatedThisFrame = true
return featherUpdate(self, dt)
end

local gameUpdate = love.update
love.update = function(dt)
DEBUGGER.__cliAutoUpdatedThisFrame = false
if gameUpdate then
gameUpdate(dt)
end
if not DEBUGGER.__cliAutoUpdatedThisFrame then
featherUpdate(DEBUGGER, dt)
end
end
end
`;
}

Expand Down
34 changes: 34 additions & 0 deletions cli/src/ui/init/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type InitMode,
type InitSetup,
} from "./model.js";
import { pluginCatalog } from "../../generated/plugin-catalog.js";

export type InitSetupState = {
mode: InitMode;
Expand Down Expand Up @@ -37,6 +38,31 @@ export type InitSetupState = {
appIdInput: string;
};

const pluginCapabilities = new Map(pluginCatalog.map((plugin) => [plugin.id, plugin.capabilities]));

export function capabilitiesForPlugins(pluginIds: Iterable<string>): string[] {
const capabilities = new Set<string>();

for (const id of pluginIds) {
for (const capability of pluginCapabilities.get(id) ?? []) {
capabilities.add(capability);
}
}

return [...capabilities].sort();
}

export function mergeCapabilities(base: string[] | "all" | undefined, pluginIds: Iterable<string>): string[] | "all" | undefined {
const pluginCaps = capabilitiesForPlugins(pluginIds);
if (pluginCaps.length === 0) return base;

if (!base || base === "all") {
return pluginCaps;
}

return [...new Set([...base, ...pluginCaps])].sort();
}

export function buildInitSetup(input: InitSetupState): InitSetup {
const config: Record<string, unknown> = {};
if (input.sessionName.trim()) config.sessionName = input.sessionName.trim();
Expand Down Expand Up @@ -68,6 +94,14 @@ export function buildInitSetup(input: InitSetupState): InitSetup {
if (input.deviceId.trim()) config.deviceId = input.deviceId.trim();
}

const mergedCapabilities = mergeCapabilities(
config.capabilities as string[] | "all" | undefined,
input.pluginPromptsEnabled ? input.include : [],
);
if (mergedCapabilities && mergedCapabilities !== "all") {
config.capabilities = mergedCapabilities;
}

if (input.needsApiKey) {
config.apiKey = input.apiKey;
config.pluginOptions = {
Expand Down
7 changes: 4 additions & 3 deletions cli/test/commands/doctor.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ test('doctor --production fails unsafe remote-control and production settings',
}
});

test('doctor --json makes missing Desktop App ID fail outside production', () => {
test('doctor --json warns for missing Desktop App ID when insecure dev override is enabled', () => {
const dir = makeTmp();
writeGame(dir);
writeFileSync(
Expand All @@ -166,10 +166,11 @@ test('doctor --json makes missing Desktop App ID fail outside production', () =>
);

const { result, parsed } = parseDoctorJsonResult(dir);
assert.equal(result.exitCode, 1, outputOf(result));
assert.equal(result.exitCode, 0, outputOf(result));
assert.equal(parsed.production, false);
const labels = new Map(parsed.checks.map((check) => [check.label, check]));
assert.equal(labels.get('Desktop App ID')?.severity, 'fail');
assert.equal(labels.get('Desktop App ID')?.severity, 'warn');
assert.equal(labels.get('Desktop App ID')?.detail, 'missing; insecure development override enabled');
assert.equal(labels.get('__DANGEROUS_INSECURE_CONNECTION__')?.severity, 'warn');
assert.equal(labels.get('Console API key')?.severity, 'warn');
assert.equal(labels.get('Network host exposure')?.severity, 'warn');
Expand Down
Loading
Loading