create-foldkit-app 0.3.2 → 0.4.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.
@@ -10,7 +10,7 @@ const validateProject = (name, projectPath, packageManager) => Effect.gen(functi
10
10
  return yield* Effect.fail(`Directory ${name} already exists!`);
11
11
  }
12
12
  const checkCommand = Command.make('which', packageManager).pipe(Command.stdout('pipe'), Command.stderr('pipe'));
13
- return yield* Command.exitCode(checkCommand).pipe(Effect.filterOrFail((exitCode) => exitCode === 0, () => `Package manager '${packageManager}' is not available. Please install it first.`));
13
+ return yield* Command.exitCode(checkCommand).pipe(Effect.filterOrFail(exitCode => exitCode === 0, () => `Package manager '${packageManager}' is not available. Please install it first.`));
14
14
  });
15
15
  const setupProject = (name, projectPath, example) => Effect.gen(function* () {
16
16
  yield* Console.log(chalk.blue('🚀 Creating your Foldkit app...'));
package/dist/index.js CHANGED
@@ -4,7 +4,7 @@ import { FetchHttpClient } from '@effect/platform';
4
4
  import { NodeContext, NodeRuntime } from '@effect/platform-node';
5
5
  import { Effect, Match, Option, Schema, String, flow } from 'effect';
6
6
  import { create as create_ } from './commands/create.js';
7
- 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))));
7
+ 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))));
8
8
  const name = Options.text('name').pipe(Options.withAlias('n'), Options.withDescription('The name of the project to create'), Options.withSchema(nameSchema));
9
9
  const example = Options.choice('example', [
10
10
  'counter',
@@ -14,7 +14,7 @@ const getBaseFiles = Effect.gen(function* () {
14
14
  yield* Match.value(stat.type).pipe(Match.when('Directory', () => processDirectory(fullPath)), Match.when('File', () => Effect.gen(function* () {
15
15
  const content = yield* fs.readFileString(fullPath);
16
16
  const relativePath = path.relative(templatesDir, fullPath);
17
- yield* Ref.update(fileContentByPath, (files) => ({
17
+ yield* Ref.update(fileContentByPath, files => ({
18
18
  ...files,
19
19
  [relativePath]: content,
20
20
  }));
@@ -40,7 +40,7 @@ const createBaseFiles = (projectPath) => Effect.gen(function* () {
40
40
  yield* fs.makeDirectory(projectPath, { recursive: true });
41
41
  const baseFiles = yield* getBaseFiles;
42
42
  yield* pipe(baseFiles, Record.toEntries, Effect.forEach(([filePath, content]) => Effect.gen(function* () {
43
- const targetPath = filePath === 'gitignore' ? '.gitignore' : filePath;
43
+ const targetPath = Match.value(filePath).pipe(Match.when('gitignore', () => '.gitignore'), Match.when('ignore', () => '.ignore'), Match.orElse(() => filePath));
44
44
  const fullPath = path.join(projectPath, targetPath);
45
45
  const dirPath = path.dirname(fullPath);
46
46
  yield* fs.makeDirectory(dirPath, { recursive: true });
@@ -51,7 +51,7 @@ const modifyBaseFiles = (projectPath, name) => Effect.gen(function* () {
51
51
  const fs = yield* FileSystem.FileSystem;
52
52
  const path = yield* Path.Path;
53
53
  const packageJsonPath = path.join(projectPath, 'package.json');
54
- return yield* fs.readFileString(packageJsonPath).pipe(Effect.map(String.replace('my-foldkit-app', name)), Effect.flatMap((updatedContent) => fs.writeFileString(packageJsonPath, updatedContent)));
54
+ return yield* fs.readFileString(packageJsonPath).pipe(Effect.map(String.replace('my-foldkit-app', name)), Effect.flatMap(updatedContent => fs.writeFileString(packageJsonPath, updatedContent)));
55
55
  });
56
56
  const GitHubFileEntry = Schema.Struct({
57
57
  name: Schema.String,
@@ -66,7 +66,7 @@ const createExampleFiles = (projectPath, example) => Effect.gen(function* () {
66
66
  const files = yield* fetchExampleFileList(example);
67
67
  const srcPath = path.join(projectPath, 'src');
68
68
  yield* fs.makeDirectory(srcPath, { recursive: true });
69
- yield* Effect.forEach(files, (file) => downloadExampleFile(file, projectPath), {
69
+ yield* Effect.forEach(files, file => downloadExampleFile(file, projectPath), {
70
70
  concurrency: 'unbounded',
71
71
  });
72
72
  });
@@ -77,7 +77,7 @@ const fetchExampleFileList = (example) => Effect.gen(function* () {
77
77
  const response = yield* client.execute(request);
78
78
  const json = yield* response.json;
79
79
  const entries = yield* Schema.decodeUnknown(Schema.Array(GitHubFileEntry))(json);
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([]))));
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
  });
83
83
  const githubApiUrl = `${GITHUB_API_BASE_URL}/${example}/src`;
@@ -94,10 +94,10 @@ const downloadExampleFile = (file, projectPath) => Effect.gen(function* () {
94
94
  const response = yield* client.execute(request);
95
95
  const content = yield* response.text;
96
96
  const pathParts = String.split(file.path, '/');
97
- const srcIndex = Array.findFirstIndex(pathParts, (part) => part === 'src');
97
+ const srcIndex = Array.findFirstIndex(pathParts, part => part === 'src');
98
98
  const relativePath = pipe(srcIndex, Option.match({
99
99
  onNone: () => file.name,
100
- onSome: (index) => pipe(pathParts, Array.drop(index + 1), Array.join('/')),
100
+ onSome: index => pipe(pathParts, Array.drop(index + 1), Array.join('/')),
101
101
  }));
102
102
  const targetPath = path.join(projectPath, 'src', relativePath);
103
103
  const dirPath = path.dirname(targetPath);
@@ -1,7 +1,7 @@
1
1
  import { Command, HttpClient, HttpClientRequest } from '@effect/platform';
2
2
  import { Array, Effect, Match, Record, Schema, pipe } from 'effect';
3
3
  const GITHUB_RAW_BASE_URL = 'https://raw.githubusercontent.com/devinjameson/foldkit/main/examples';
4
- 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));
4
+ 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));
5
5
  const StringRecord = Schema.Record({ key: Schema.String, value: Schema.String });
6
6
  const PackageJson = Schema.Struct({
7
7
  dependencies: Schema.optionalWith(StringRecord, { default: () => ({}) }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-foldkit-app",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Create Foldkit applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "semi": false,
3
3
  "singleQuote": true,
4
+ "arrowParens": "avoid",
4
5
  "trailingComma": "all",
5
6
  "printWidth": 100,
6
7
 
@@ -0,0 +1,138 @@
1
+ # Agent Development Notes
2
+
3
+ This is a Foldkit app — a framework built on Effect-TS.
4
+
5
+ If `./foldkit` exists, it contains the full Foldkit repository as a git submodule. The `examples/` directory and `packages/typing-game/client/src/` in particular are the best reference for what idiomatic Foldkit apps look like.
6
+
7
+ If `./foldkit` does not exist and the line below says `submodule_prompted: false`, offer to run `git submodule add https://github.com/devinjameson/foldkit.git` — this gives you access to the full source, examples, and docs. Whether the user accepts or declines, update the value to `true` so it isn't asked again.
8
+
9
+ submodule_prompted: false
10
+
11
+ ## Project Conventions
12
+
13
+ - Foldkit is tightly coupled to the Effect ecosystem. Do not suggest solutions outside of Effect-TS.
14
+ - Model fields must be Schema types (the model is a schema). Plain TypeScript types are fine elsewhere — function return types, local variables, etc.
15
+ - Use full names like `Message` (not `Msg`), and `withReturnType` (not `as const` or type casting).
16
+ - Use `m()` for message schemas, `ts()` for other tagged structs (model states, field validation), and `r()` for route schemas.
17
+ - Every message union should include a `NoOp` variant: `const NoOp = m('NoOp')`.
18
+
19
+ ## Foldkit Patterns
20
+
21
+ ### Update
22
+
23
+ `init` and `update` both return `[Model, ReadonlyArray<Command<Message>>]`:
24
+
25
+ ```ts
26
+ type UpdateReturn = [Model, ReadonlyArray<Command<Message>>]
27
+ const withUpdateReturn = M.withReturnType<UpdateReturn>()
28
+
29
+ const update = (model: Model, message: Message): UpdateReturn =>
30
+ M.value(message).pipe(
31
+ withUpdateReturn,
32
+ M.tagsExhaustive({
33
+ NoOp: () => [model, []],
34
+ ClickedIncrement: () => [evo(model, { count: count => count + 1 }), []],
35
+ }),
36
+ )
37
+ ```
38
+
39
+ ### Model Updates with `evo`
40
+
41
+ Use `evo()` for immutable model updates — never spread or Object.assign:
42
+
43
+ ```ts
44
+ evo(model, { isSubmitting: () => true })
45
+ evo(model, { count: count => count + 1 })
46
+ ```
47
+
48
+ Don't add type annotations to `evo` callbacks when the type can be inferred.
49
+
50
+ ### View
51
+
52
+ Call `html<Message>()` once in a dedicated `html.ts` file and import the destructured helpers everywhere else:
53
+
54
+ ```ts
55
+ // html.ts
56
+ export const { div, button, span, Class, OnClick } = html<Message>()
57
+ ```
58
+
59
+ Use `empty` (not `null`) for conditional rendering. Use `M.value().pipe(M.tagsExhaustive({...}))` for rendering discriminated unions and `Array.match` for rendering lists that may be empty.
60
+
61
+ Use `keyed` wrappers whenever the view branches into structurally different layouts based on route or model state. Without keying, the virtual DOM will try to diff one layout into another (e.g. a full-width landing page into a sidebar docs layout), which causes stale DOM, mismatched event handlers, and subtle rendering bugs. Key the outermost container of each layout branch with a stable string (e.g. `keyed('div')('landing', ...)` vs `keyed('div')('docs', ...)`). Within a single layout, key the content area on the route tag (e.g. `keyed('div')(model.route._tag, ...)`) so page transitions replace rather than patch.
62
+
63
+ ### Commands
64
+
65
+ Commands catch all errors and return messages — side effects never crash the app:
66
+
67
+ ```ts
68
+ const fetchWeather = (
69
+ city: string,
70
+ ): Command<typeof SucceededWeatherFetch | typeof FailedWeatherFetch> =>
71
+ Effect.gen(function* () {
72
+ // ...
73
+ return SucceededWeatherFetch({ data })
74
+ }).pipe(Effect.catchAll(error => Effect.succeed(FailedWeatherFetch({ error: String(error) }))))
75
+ ```
76
+
77
+ Commands return specific schema types (e.g. `Command<typeof SucceededMsg | typeof FailedMsg>`) rather than the full Message type.
78
+
79
+ ### File Organization
80
+
81
+ Use uppercase section headers (`// MODEL`, `// MESSAGE`, `// INIT`, `// UPDATE`, `// COMMAND`, `// VIEW`) to make files easier to skim. These are for wayfinding — they make it clear where things live and where new code should go. Use domain-specific headers too when it helps (e.g. `// PHYSICS`, `// ROUTING`).
82
+
83
+ Even after extracting some sections to their own files (e.g. `message.ts`), the remaining file may still benefit from headers. Extract to separate files when it helps with organization.
84
+
85
+ ## Code Quality Standards
86
+
87
+ - Every name should eliminate ambiguity. Prefix Option-typed values with `maybe` (e.g. `maybeSession`). Name functions by their precise effect (e.g. `enqueueMessage` not `addMessage`). A reader should never need to check a type signature to understand what a name refers to.
88
+ - Each function should operate at a single abstraction level. Orchestrators delegate to focused helpers — they don't mix coordination with implementation. If a function reads like it's doing two things, extract one.
89
+ - Encode state in discriminated unions, not booleans or nullable fields. Use `Idle | Loading | Error | Ok` instead of `isLoading: boolean`. Make impossible states unrepresentable.
90
+ - Name messages as verb-first, past-tense events describing what happened (`ClickedSubmit`, `GotWeatherData`, `UpdatedSearchInput`), not imperative commands. The verb prefix acts as a category marker: `Clicked*` for button presses, `Updated*` for input changes, `Requested*` for async triggers, `Got*` for data responses. The update function decides what to do — messages are facts.
91
+ - Use `Option` instead of `null` or `undefined`. Match explicitly with `Option.match` or chain with `Option.map`/`Option.flatMap`. No `if (x != null)` checks. Prefer `Option.match` over `Option.map` + `Option.getOrElse` — if you're unwrapping at the end, just match.
92
+ - Prefer curried, data-last functions that compose in `pipe` chains.
93
+ - Every line should serve a purpose. No dead code, no empty catch blocks, no placeholder types, no defensive code for impossible cases.
94
+
95
+ ## Code Style Conventions
96
+
97
+ ### Effect-TS Patterns
98
+
99
+ - Prefer `pipe()` for multi-step data flow. Never use `pipe` with a single operation — call the function directly instead: `Option.match(value, {...})` not `pipe(value, Option.match({...}))`.
100
+ - Use `Effect.gen()` for imperative-style async operations.
101
+ - Always use Effect.Match instead of switch.
102
+ - 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. Exception: native `.map`, `.filter`, etc. are fine when calling directly on a named variable — use Effect's `Array.map` in `pipe` chains where the curried, data-last form composes naturally.
103
+ - 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.
104
+ - Never cast Schema values with `as Type`. Use callable constructors: `LoginSucceeded({ sessionId })` not `{ _tag: 'LoginSucceeded', sessionId } as Message`.
105
+ - 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.
106
+ - Use `Array.take` instead of `.slice(0, n)`.
107
+ - 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`.
108
+
109
+ ### Message Layout
110
+
111
+ Message definitions follow a strict layout:
112
+
113
+ ```ts
114
+ const NoOp = m('NoOp')
115
+ const ClickedSubmit = m('ClickedSubmit')
116
+ const ChangedEmail = m('ChangedEmail', { value: S.String })
117
+
118
+ const Message = S.Union(NoOp, ClickedSubmit, ChangedEmail)
119
+ type Message = typeof Message.Type
120
+ ```
121
+
122
+ 1. **Values** — all `m()` declarations, no blank lines between them
123
+ 2. **Union + type** — `S.Union(...)` followed by `type Message = typeof Message.Type` on adjacent lines (no blank line between them)
124
+
125
+ Use `typeof ClickedSubmit` in type positions (e.g. `Command<typeof ClickedSubmit>`) to reference a schema value's type.
126
+
127
+ ### General Preferences
128
+
129
+ - Never abbreviate names. Use full, descriptive names everywhere — variables, types, functions, parameters, including callback parameters. e.g. `signature` not `sig`, `Message` not `Msg`, `(tickCount) => tickCount + 1` not `(t) => t + 1`.
130
+ - Avoid `let`. Use `const` and prefer immutable patterns.
131
+ - Always use braces for control flow. `if (foo) { return true }` not `if (foo) return true`.
132
+ - Use `is*` for boolean naming e.g. `isPlaying`, `isValid`.
133
+ - 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.
134
+ - Use capitalized string literals for Schema literal types: `S.Literal('Horizontal', 'Vertical')` not `S.Literal('horizontal', 'vertical')`.
135
+ - Capitalize namespace imports: `import * as Command from './command'` not `import * as command from './command'`.
136
+ - Extract magic numbers to named constants. No raw numeric literals in logic.
137
+ - Never use `T[]` syntax. Always use `Array<T>` or `ReadonlyArray<T>`.
138
+ - Extract repeated inline style values (colors, shadows) to constants.
@@ -0,0 +1 @@
1
+ foldkit/