create-foldkit-app 0.6.3 → 0.7.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 CHANGED
@@ -5,14 +5,14 @@ Scaffolding CLI for new Foldkit applications. Creates a ready-to-run project wit
5
5
  ## Usage
6
6
 
7
7
  ```bash
8
- npx create-foldkit-app --wizard
8
+ npx create-foldkit-app
9
9
  # or
10
- pnpm create foldkit-app --wizard
10
+ pnpm create foldkit-app
11
11
  # or
12
- yarn create foldkit-app --wizard
12
+ yarn create foldkit-app
13
13
  ```
14
14
 
15
- The CLI will prompt you for a project name, starter example, and package manager.
15
+ The CLI prompts you for a project name, starter example, and package manager. Pass `--name`, `--example`, and/or `--package-manager` to skip the matching prompts.
16
16
 
17
17
  ## Examples
18
18
 
@@ -1,17 +1,63 @@
1
- import { Command, FileSystem, Path } from '@effect/platform';
2
1
  import chalk from 'chalk';
3
- import { Console, Effect, Match } from 'effect';
2
+ import { Console, Effect, FileSystem, Match, Option, Path } from 'effect';
3
+ import { Prompt } from 'effect/unstable/cli';
4
+ import { spawnSync } from 'node:child_process';
5
+ import { examples } from '../examples.js';
4
6
  import { createProject } from '../utils/files.js';
5
7
  import { installDependencies } from '../utils/packages.js';
8
+ import { validateProjectName } from '../validateName.js';
6
9
  const isWindows = process.platform === 'win32';
10
+ const promptForName = Prompt.text({
11
+ message: 'Give your project a name',
12
+ validate: value => Option.match(validateProjectName(value), {
13
+ onNone: () => Effect.succeed(value),
14
+ onSome: message => Effect.fail(message),
15
+ }),
16
+ });
17
+ const promptForExample = Prompt.autoComplete({
18
+ message: 'Pick a starting example',
19
+ choices: examples.map(({ value, title, description }) => ({
20
+ value,
21
+ title,
22
+ description,
23
+ })),
24
+ });
25
+ const promptForPackageManager = Prompt.select({
26
+ message: 'Pick a package manager',
27
+ choices: [
28
+ { value: 'pnpm', title: 'pnpm' },
29
+ { value: 'npm', title: 'npm' },
30
+ { value: 'yarn', title: 'yarn' },
31
+ ],
32
+ });
33
+ const resolveInput = (input) => Effect.gen(function* () {
34
+ const name = yield* Option.match(input.name, {
35
+ onNone: () => promptForName,
36
+ onSome: Effect.succeed,
37
+ });
38
+ const example = yield* Option.match(input.example, {
39
+ onNone: () => promptForExample,
40
+ onSome: Effect.succeed,
41
+ });
42
+ const packageManager = yield* Option.match(input.packageManager, {
43
+ onNone: () => promptForPackageManager,
44
+ onSome: Effect.succeed,
45
+ });
46
+ return { name, example, packageManager };
47
+ });
7
48
  const validateProject = (name, projectPath, packageManager) => Effect.gen(function* () {
8
49
  const fs = yield* FileSystem.FileSystem;
9
50
  const exists = yield* fs.exists(projectPath);
10
51
  if (exists) {
11
52
  return yield* Effect.fail(`Directory ${name} already exists!`);
12
53
  }
13
- const checkCommand = Command.make(isWindows ? 'where' : 'which', packageManager).pipe(Command.stdout('pipe'), Command.stderr('pipe'));
14
- return yield* Command.exitCode(checkCommand).pipe(Effect.filterOrFail(exitCode => exitCode === 0, () => `Package manager '${packageManager}' is not available. Please install it first.`));
54
+ const exitCode = yield* Effect.sync(() => {
55
+ const result = spawnSync(isWindows ? 'where' : 'which', [packageManager], { stdio: 'pipe', shell: isWindows });
56
+ return result.status;
57
+ });
58
+ if (exitCode !== 0) {
59
+ return yield* Effect.fail(`Package manager '${packageManager}' is not available. Please install it first.`);
60
+ }
15
61
  });
16
62
  const setupProject = (name, projectPath, example) => Effect.gen(function* () {
17
63
  yield* Console.log(chalk.blue('🚀 Creating your Foldkit app...'));
@@ -44,24 +90,27 @@ const displaySuccessMessage = (name, packageManager) => Effect.gen(function* ()
44
90
  yield* Console.log('');
45
91
  yield* Console.log(` Details: ${chalk.cyan('foldkit.dev/ai/overview')}`);
46
92
  yield* Console.log('');
47
- yield* Console.log('Foldkit is a one-astronaut nights-and-weekends project.\n' +
48
- 'If you have praise or criticism, do share.\n' +
49
- "Please. It's lonely out here.\n\n" +
50
- 'Be careful. Make good decisions.');
51
- yield* Console.log('');
52
93
  yield* Console.log(`Training manual: ${chalk.cyan('foldkit.dev')}`);
53
94
  yield* Console.log(`Incident report: ${chalk.cyan('github.com/foldkit/foldkit/issues')}`);
54
95
  yield* Console.log('');
55
- yield* Console.log(`Transmissions: ${chalk.cyan('foldkit.dev/newsletter')}`);
56
- yield* Console.log(`X: ${chalk.cyan('x.com/devinjameson')}`);
57
- yield* Console.log(`Bluesky: ${chalk.cyan('bsky.app/profile/devinjameson.bsky.social')}`);
58
- yield* Console.log(`Threads: ${chalk.cyan('threads.com/@devinthedeveloper')}`);
96
+ yield* Console.log(`Crew channel: ${chalk.cyan('discord.gg/AZRTEs2VX')}`);
97
+ yield* Console.log('');
98
+ yield* Console.log('Transmissions:');
99
+ yield* Console.log(` Newsletter: ${chalk.cyan('foldkit.dev/newsletter')}`);
100
+ yield* Console.log(` X: ${chalk.cyan('x.com/devinjameson')}`);
101
+ yield* Console.log(` Bluesky: ${chalk.cyan('bsky.app/profile/devinjameson.bsky.social')}`);
102
+ yield* Console.log(` Threads: ${chalk.cyan('threads.com/@devinthedeveloper')}`);
103
+ yield* Console.log('');
104
+ yield* Console.log('Foldkit is a one-astronaut nights-and-weekends project.\n' +
105
+ 'If you have praise or criticism, do share.\n' +
106
+ "Please. It's lonely out here.");
59
107
  yield* Console.log('');
60
108
  yield* Console.log('Love you,');
61
109
  yield* Console.log('Mission Control');
62
110
  yield* Console.log('');
63
111
  });
64
- export const create = ({ name, example, packageManager }) => Effect.gen(function* () {
112
+ export const create = (input) => Effect.gen(function* () {
113
+ const { name, example, packageManager } = yield* resolveInput(input);
65
114
  const path = yield* Path.Path;
66
115
  const projectPath = path.resolve(name);
67
116
  yield* validateProject(name, projectPath, packageManager);
@@ -0,0 +1,100 @@
1
+ export const EXAMPLE_VALUES = [
2
+ 'counter',
3
+ 'todo',
4
+ 'stopwatch',
5
+ 'crash-view',
6
+ 'form',
7
+ 'job-application',
8
+ 'weather',
9
+ 'routing',
10
+ 'query-sync',
11
+ 'snake',
12
+ 'auth',
13
+ 'shopping-cart',
14
+ 'pixel-art',
15
+ 'websocket-chat',
16
+ 'kanban',
17
+ 'ui-showcase',
18
+ ];
19
+ export const examples = [
20
+ {
21
+ value: 'counter',
22
+ title: 'counter',
23
+ description: 'Simple increment/decrement with reset',
24
+ },
25
+ {
26
+ value: 'todo',
27
+ title: 'todo',
28
+ description: 'CRUD operations with localStorage persistence',
29
+ },
30
+ {
31
+ value: 'stopwatch',
32
+ title: 'stopwatch',
33
+ description: 'Timer with start/stop/reset functionality',
34
+ },
35
+ {
36
+ value: 'crash-view',
37
+ title: 'crash-view',
38
+ description: 'Custom crash fallback UI with crash.view and crash.report',
39
+ },
40
+ {
41
+ value: 'form',
42
+ title: 'form',
43
+ description: 'Form validation with async email checking',
44
+ },
45
+ {
46
+ value: 'job-application',
47
+ title: 'job-application',
48
+ description: 'Multi-step form with async validation, file uploads, and per-step error indicators',
49
+ },
50
+ {
51
+ value: 'weather',
52
+ title: 'weather',
53
+ description: 'HTTP requests with async state handling',
54
+ },
55
+ {
56
+ value: 'routing',
57
+ title: 'routing',
58
+ description: 'URL routing with parser combinators and route parameters',
59
+ },
60
+ {
61
+ value: 'query-sync',
62
+ title: 'query-sync',
63
+ description: 'URL-driven filtering, sorting, and search with query parameters',
64
+ },
65
+ {
66
+ value: 'snake',
67
+ title: 'snake',
68
+ description: 'Classic game built with subscriptions',
69
+ },
70
+ {
71
+ value: 'auth',
72
+ title: 'auth',
73
+ description: 'Authentication with Submodels, OutMessage, and protected routes',
74
+ },
75
+ {
76
+ value: 'shopping-cart',
77
+ title: 'shopping-cart',
78
+ description: 'Complex state management with nested models and routing',
79
+ },
80
+ {
81
+ value: 'pixel-art',
82
+ title: 'pixel-art',
83
+ description: 'Pixel editor with undo/redo, time-travel history, UI components, and localStorage persistence',
84
+ },
85
+ {
86
+ value: 'websocket-chat',
87
+ title: 'websocket-chat',
88
+ description: 'Managed resources with WebSocket integration',
89
+ },
90
+ {
91
+ value: 'kanban',
92
+ title: 'kanban',
93
+ description: 'Drag-and-drop board with fractional indexing, keyboard navigation, and screen reader announcements',
94
+ },
95
+ {
96
+ value: 'ui-showcase',
97
+ title: 'ui-showcase',
98
+ description: 'Every Foldkit UI component with routing and Submodels',
99
+ },
100
+ ];
package/dist/index.js CHANGED
@@ -1,62 +1,34 @@
1
1
  #!/usr/bin/env node
2
- import { Command, HelpDoc, Options } from '@effect/cli';
3
- import { FetchHttpClient } from '@effect/platform';
4
- import { NodeContext, NodeRuntime } from '@effect/platform-node';
5
- import { Effect, Match, Option, Schema, String, flow } from 'effect';
2
+ import { NodeRuntime, NodeServices, NodeStdio } from '@effect/platform-node';
3
+ import { Effect, Layer, Option, Schema } from 'effect';
4
+ import { Command, Flag } from 'effect/unstable/cli';
5
+ import { FetchHttpClient } from 'effect/unstable/http';
6
6
  import { createRequire } from 'node:module';
7
7
  import { create as create_ } from './commands/create.js';
8
+ import { EXAMPLE_VALUES } from './examples.js';
9
+ import { validateProjectName } from './validateName.js';
8
10
  /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
9
11
  const packageJson = createRequire(import.meta.url)('../package.json');
10
- const nameSchema = Schema.String.pipe(Schema.filter(name => Match.value(name).pipe(Match.whenOr(String.includes('/'), String.includes('\\'), () => 'Project name cannot contain path separators (/ or \\)'), Match.when(String.includes(' '), () => 'Project name cannot contain spaces'), Match.when(flow(String.match(/[<>:"|?*]/), Option.isSome), () => 'Project name cannot contain special characters: < > : " | ? *'), Match.whenOr(String.startsWith('.'), String.startsWith('-'), () => 'Project name cannot start with . or -'), Match.when(String.isEmpty, () => 'Project name cannot be empty'), Match.orElse(() => true))));
11
- const name = Options.text('name').pipe(Options.withAlias('n'), Options.withDescription('The name of the project to create'), Options.withSchema(nameSchema));
12
- const example = Options.choice('example', [
13
- 'counter',
14
- 'todo',
15
- 'stopwatch',
16
- 'crash-view',
17
- 'form',
18
- 'job-application',
19
- 'weather',
20
- 'routing',
21
- 'query-sync',
22
- 'snake',
23
- 'auth',
24
- 'shopping-cart',
25
- 'pixel-art',
26
- 'websocket-chat',
27
- 'kanban',
28
- 'ui-showcase',
29
- ]).pipe(Options.withAlias('e'), Options.withDescription("The example application to start from. Pick an example that's similar to the application you're building. Or create multiple projects and take pieces of each!\n\n" +
30
- 'Available examples:\n' +
31
- ' counter - Simple increment/decrement with reset\n' +
32
- ' todo - CRUD operations with localStorage persistence\n' +
33
- ' stopwatch - Timer with start/stop/reset functionality\n' +
34
- ' crash-view - Custom crash fallback UI with crash.view and crash.report\n' +
35
- ' form - Form validation with async email checking\n' +
36
- ' job-application - Multi-step form with async validation, file uploads, and per-step error indicators\n' +
37
- ' weather - HTTP requests with async state handling\n' +
38
- ' routing - URL routing with parser combinators and route parameters\n' +
39
- ' query-sync - URL-driven filtering, sorting, and search with query parameters\n' +
40
- ' snake - Classic game built with subscriptions\n' +
41
- ' auth - Authentication with Submodels, OutMessage, and protected routes\n' +
42
- ' shopping-cart - Complex state management with nested models and routing\n' +
43
- ' pixel-art - Pixel editor with undo/redo, time-travel history, UI components, and localStorage persistence\n' +
44
- ' websocket-chat - Managed resources with WebSocket integration\n' +
45
- ' kanban - Drag-and-drop board with fractional indexing, keyboard navigation, and screen reader announcements\n' +
46
- ' ui-showcase - Every Foldkit UI component with routing and Submodels'));
47
- const packageManager = Options.choice('package-manager', [
12
+ const nameSchema = Schema.String.pipe(Schema.check(Schema.makeFilter(value => Option.match(validateProjectName(value), {
13
+ onNone: () => true,
14
+ onSome: message => message,
15
+ }))));
16
+ const name = Flag.string('name').pipe(Flag.withAlias('n'), Flag.withDescription('The name of the project to create'), Flag.withSchema(nameSchema), Flag.optional);
17
+ const example = Flag.choice('example', EXAMPLE_VALUES).pipe(Flag.withAlias('e'), Flag.withDescription("The example application to start from. Run with no flags for an interactive picker that shows each example's description."), Flag.optional);
18
+ const packageManager = Flag.choice('package-manager', [
48
19
  'pnpm',
49
20
  'npm',
50
21
  'yarn',
51
- ]).pipe(Options.withAlias('p'), Options.withDescription('The package manager to use for installing dependencies'));
22
+ ]).pipe(Flag.withAlias('p'), Flag.withDescription('The package manager to use for installing dependencies'), Flag.optional);
52
23
  const create = Command.make('create', {
53
24
  name,
54
25
  example,
55
26
  packageManager,
56
- }, create_);
27
+ }, create_).pipe(Command.withDescription('Create a new Foldkit application'));
57
28
  const cli = Command.run(create, {
58
- name: 'Create Foldkit App',
59
29
  version: packageJson.version,
60
- summary: HelpDoc.getSpan(HelpDoc.p('Create a new Foldkit application')),
61
30
  });
62
- cli(process.argv).pipe(Effect.provide([FetchHttpClient.layer, NodeContext.layer]), NodeRuntime.runMain);
31
+ cli.pipe(Effect.provide([
32
+ FetchHttpClient.layer,
33
+ Layer.mergeAll(NodeServices.layer, NodeStdio.layer),
34
+ ]), NodeRuntime.runMain);
@@ -1,5 +1,5 @@
1
- import { FileSystem, HttpClient, HttpClientRequest, Path, } from '@effect/platform';
2
- import { Array, Effect, Match, Option, Record, Ref, Schema, String, pipe, } from 'effect';
1
+ import { Array, Effect, FileSystem, Match, Option, Path, Record, Ref, Schema, String, pipe, } from 'effect';
2
+ import { HttpClient, HttpClientRequest, } from 'effect/unstable/http';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  const GITHUB_API_BASE_URL = 'https://api.github.com/repos/foldkit/foldkit/contents/examples';
5
5
  const getBaseFiles = Effect.gen(function* () {
@@ -76,7 +76,7 @@ const fetchExampleFileList = (example) => Effect.gen(function* () {
76
76
  const request = HttpClientRequest.get(apiUrl);
77
77
  const response = yield* client.execute(request);
78
78
  const json = yield* response.json;
79
- const entries = yield* Schema.decodeUnknown(Schema.Array(GitHubFileEntry))(json);
79
+ const entries = yield* Schema.decodeUnknownEffect(Schema.Array(GitHubFileEntry))(json);
80
80
  const results = yield* Effect.forEach(entries, entry => Match.value(entry.type).pipe(Match.when('file', () => Effect.succeed([entry])), Match.when('dir', () => fetchFilesRecursively(entry.url)), Match.orElse(() => Effect.succeed([]))));
81
81
  return Array.flatten(results);
82
82
  });
@@ -1,12 +1,13 @@
1
- import { Command, HttpClient, HttpClientRequest } from '@effect/platform';
2
1
  import { Array, Effect, Match, Record, Schema, pipe } from 'effect';
2
+ import { HttpClient, HttpClientRequest } from 'effect/unstable/http';
3
+ import { spawn } from 'node:child_process';
3
4
  const GITHUB_RAW_BASE_URL = 'https://raw.githubusercontent.com/foldkit/foldkit/main/examples';
4
5
  const isWindows = process.platform === 'win32';
5
6
  const getInstallArgs = (packageManager, isDev = false) => pipe(Match.value(packageManager), Match.when('npm', () => ['install']), Match.when('yarn', () => ['add']), Match.when('pnpm', () => ['add']), Match.exhaustive, args => (isDev ? [...args, '-D'] : args));
6
- const StringRecord = Schema.Record({ key: Schema.String, value: Schema.String });
7
+ const StringRecord = Schema.Record(Schema.String, Schema.String);
7
8
  const PackageJson = Schema.Struct({
8
- dependencies: Schema.optionalWith(StringRecord, { default: () => ({}) }),
9
- devDependencies: Schema.optionalWith(StringRecord, { default: () => ({}) }),
9
+ dependencies: StringRecord.pipe(Schema.withDecodingDefaultKey(Effect.succeed({}))),
10
+ devDependencies: StringRecord.pipe(Schema.withDecodingDefaultKey(Effect.succeed({}))),
10
11
  });
11
12
  const formatDeps = (deps) => pipe(deps, Record.toEntries, Array.filter(([_, version]) => !version.includes('workspace:')), Array.map(([name, version]) => `${name}@${version}`));
12
13
  const fetchExampleDeps = (example) => Effect.gen(function* () {
@@ -14,18 +15,47 @@ const fetchExampleDeps = (example) => Effect.gen(function* () {
14
15
  const url = `${GITHUB_RAW_BASE_URL}/${example}/package.json`;
15
16
  const response = yield* client.execute(HttpClientRequest.get(url));
16
17
  const json = yield* response.json;
17
- const packageJson = yield* Schema.decodeUnknown(PackageJson)(json);
18
+ const packageJson = yield* Schema.decodeUnknownEffect(PackageJson)(json);
18
19
  return {
19
20
  dependencies: formatDeps(packageJson.dependencies),
20
21
  devDependencies: formatDeps(packageJson.devDependencies),
21
22
  };
22
23
  });
24
+ const runCommand = (command, args, cwd) => Effect.callback((resume) => {
25
+ const child = spawn(command, [...args], {
26
+ cwd,
27
+ shell: isWindows,
28
+ stdio: 'inherit',
29
+ });
30
+ child.on('error', error => resume(Effect.fail(error)));
31
+ child.on('exit', code => {
32
+ if (code === 0) {
33
+ resume(Effect.void);
34
+ }
35
+ else {
36
+ resume(Effect.fail(new Error(`${command} exited with code ${code}`)));
37
+ }
38
+ });
39
+ // NOTE: SIGTERM only — the Effect.callback finalizer is sync so we
40
+ // can't escalate to SIGKILL. On Windows with shell:true the signal
41
+ // hits cmd.exe but doesn't propagate to the package manager.
42
+ return Effect.sync(() => {
43
+ if (child.exitCode === null && !child.killed) {
44
+ child.kill();
45
+ }
46
+ });
47
+ });
23
48
  export const installDependencies = (projectPath, packageManager, example) => Effect.gen(function* () {
24
49
  const exampleDeps = yield* fetchExampleDeps(example);
25
50
  const installArgs = getInstallArgs(packageManager);
26
- const installDeps = Command.make(packageManager, ...installArgs, 'foldkit', ...exampleDeps.dependencies).pipe(Command.runInShell(isWindows), Command.workingDirectory(projectPath), Command.stdout('inherit'), Command.stderr('inherit'));
27
- yield* Command.exitCode(installDeps);
51
+ yield* runCommand(packageManager, [...installArgs, 'foldkit', ...exampleDeps.dependencies], projectPath);
28
52
  const installDevArgs = getInstallArgs(packageManager, true);
29
- const installDevDeps = Command.make(packageManager, ...installDevArgs, '@foldkit/vite-plugin', '@foldkit/devtools-mcp', 'vitest', 'happy-dom', ...exampleDeps.devDependencies).pipe(Command.runInShell(isWindows), Command.workingDirectory(projectPath), Command.stdout('inherit'), Command.stderr('inherit'));
30
- yield* Command.exitCode(installDevDeps);
53
+ yield* runCommand(packageManager, [
54
+ ...installDevArgs,
55
+ '@foldkit/vite-plugin',
56
+ '@foldkit/devtools-mcp',
57
+ 'vitest',
58
+ 'happy-dom',
59
+ ...exampleDeps.devDependencies,
60
+ ], projectPath);
31
61
  });
@@ -0,0 +1,2 @@
1
+ import { Match, Option, String, flow } from 'effect';
2
+ export const validateProjectName = (name) => Match.value(name).pipe(Match.when(String.isEmpty, () => Option.some('Project name cannot be empty')), Match.whenOr(String.includes('/'), String.includes('\\'), () => Option.some('Project name cannot contain path separators (/ or \\)')), Match.when(String.includes(' '), () => Option.some('Project name cannot contain spaces')), Match.when(flow(String.match(/[<>:"|?*]/), Option.isSome), () => Option.some('Project name cannot contain special characters: < > : " | ? *')), Match.whenOr(String.startsWith('.'), String.startsWith('-'), () => Option.some('Project name cannot start with . or -')), Match.orElse(() => Option.none()));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-foldkit-app",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "description": "Create Foldkit applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,13 +12,9 @@
12
12
  "templates"
13
13
  ],
14
14
  "dependencies": {
15
- "@effect/cli": "^0.73.2",
16
- "@effect/platform": "^0.94.5",
17
- "@effect/platform-node": "^0.104.1",
18
- "@types/prompts": "^2.4.9",
15
+ "@effect/platform-node": "4.0.0-beta.59",
19
16
  "chalk": "^5.6.2",
20
- "effect": "^3.19.19",
21
- "prompts": "^2.4.2",
17
+ "effect": "4.0.0-beta.59",
22
18
  "rimraf": "^6.1.2",
23
19
  "typescript": "^6.0.2"
24
20
  },
@@ -76,7 +76,7 @@ const fetchWeather = (city: string) =>
76
76
  // ...
77
77
  return SucceededFetchWeather({ data })
78
78
  }).pipe(
79
- Effect.catchAll(error =>
79
+ Effect.catch(error =>
80
80
  Effect.succeed(FailedFetchWeather({ error: String(error) })),
81
81
  ),
82
82
  FetchWeather,
@@ -154,7 +154,7 @@ If the `foldkit_*` tools aren't visible, see `@foldkit/devtools-mcp` on npm for
154
154
  - Prefer Effect module functions over native methods when available — e.g. `Array.map`, `Array.filter`, `Option.map`, `String.startsWith` from Effect instead of their native equivalents. This includes Effect's `String` module: use `String.includes`, `String.indexOf` (returns `Option<number>`), `String.slice`, `String.startsWith`, `String.replaceAll`, `String.length`, `String.isNonEmpty`, `String.trim` etc. in `pipe` chains. Exception: native `.map`, `.filter`, `.indexOf()`, `.slice()`, etc. are fine when calling directly on a named variable — use Effect's curried, data-last forms in `pipe` chains where they compose naturally.
155
155
  - Never use `for` loops or `let` for iteration. Use `Array.makeBy` for index-based construction, `Array.range` + `Array.findFirst`/`Array.findLast` for searches, and `Array.filterMap`/`Array.flatMap` for transforms.
156
156
  - Never cast Schema values with `as Type`. Use callable constructors: `LoginSucceeded({ sessionId })` not `{ _tag: 'LoginSucceeded', sessionId } as Message`.
157
- - Use `Option` for model fields that may be absent — not empty strings or zero values. `loginError: S.OptionFromSelf(S.String)` not `loginError: S.String` with `''` as the "none" state. Use `Option.match` in views to conditionally render.
157
+ - Use `Option` for model fields that may be absent — not empty strings or zero values. `loginError: S.Option(S.String)` not `loginError: S.String` with `''` as the "none" state. Use `Option.match` in views to conditionally render.
158
158
  - Use `Array.take` instead of `.slice(0, n)`.
159
159
  - Always use `Array.isEmptyArray(foo)` instead of `foo.length === 0`. Use `Array.isNonEmptyArray(foo)` for non-empty checks. When handling both cases, prefer `Array.match`.
160
160
 
@@ -167,12 +167,16 @@ const ClickedSubmit = m('ClickedSubmit')
167
167
  const ChangedEmail = m('ChangedEmail', { value: S.String })
168
168
  const CompletedNavigateInternal = m('CompletedNavigateInternal')
169
169
 
170
- const Message = S.Union(ClickedSubmit, ChangedEmail, CompletedNavigateInternal)
170
+ const Message = S.Union([
171
+ ClickedSubmit,
172
+ ChangedEmail,
173
+ CompletedNavigateInternal,
174
+ ])
171
175
  type Message = typeof Message.Type
172
176
  ```
173
177
 
174
178
  1. **Values** — all `m()` declarations, no blank lines between them
175
- 2. **Union + type** — `S.Union(...)` followed by `type Message = typeof Message.Type` on adjacent lines (no blank line between them)
179
+ 2. **Union + type** — `S.Union([...])` followed by `type Message = typeof Message.Type` on adjacent lines (no blank line between them)
176
180
 
177
181
  Use `typeof ClickedSubmit` in type positions to reference a schema value's type.
178
182
 
@@ -184,7 +188,7 @@ Use `typeof ClickedSubmit` in type positions to reference a schema value's type.
184
188
  - Always use braces for control flow. `if (foo) { return true }` not `if (foo) return true`.
185
189
  - Use `is*` for boolean naming e.g. `isPlaying`, `isValid`.
186
190
  - Don't add inline or block comments to explain code — if code needs explanation, refactor for clarity or use better names. Exceptions: section headers (see File Organization above) and TSDoc (`/** ... */`) on public exports.
187
- - Use capitalized string literals for Schema literal types: `S.Literal('Horizontal', 'Vertical')` not `S.Literal('horizontal', 'vertical')`.
191
+ - Use capitalized string literals for Schema literal types: `S.Literals(['Horizontal', 'Vertical'])` not `S.Literals(['horizontal', 'vertical'])`.
188
192
  - Capitalize namespace imports: `import * as Command from './command'` not `import * as command from './command'`.
189
193
  - Extract magic numbers to named constants. No raw numeric literals in logic.
190
194
  - Never use `T[]` syntax. Always use `Array<T>` or `ReadonlyArray<T>`.