create-foldkit-app 0.6.3 → 0.7.1
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 +4 -4
- package/dist/commands/create.js +63 -14
- package/dist/examples.js +100 -0
- package/dist/index.js +19 -47
- package/dist/utils/files.js +3 -3
- package/dist/utils/packages.js +39 -9
- package/dist/validateName.js +2 -0
- package/package.json +5 -9
- package/templates/base/AGENTS.md +27 -16
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
|
|
8
|
+
npx create-foldkit-app
|
|
9
9
|
# or
|
|
10
|
-
pnpm create foldkit-app
|
|
10
|
+
pnpm create foldkit-app
|
|
11
11
|
# or
|
|
12
|
-
yarn create foldkit-app
|
|
12
|
+
yarn create foldkit-app
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
The CLI
|
|
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
|
|
package/dist/commands/create.js
CHANGED
|
@@ -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
|
|
14
|
-
|
|
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(`
|
|
56
|
-
yield* Console.log(
|
|
57
|
-
yield* Console.log(
|
|
58
|
-
yield* Console.log(`
|
|
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 = (
|
|
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);
|
package/dist/examples.js
ADDED
|
@@ -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 {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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(
|
|
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
|
|
31
|
+
cli.pipe(Effect.provide([
|
|
32
|
+
FetchHttpClient.layer,
|
|
33
|
+
Layer.mergeAll(NodeServices.layer, NodeStdio.layer),
|
|
34
|
+
]), NodeRuntime.runMain);
|
package/dist/utils/files.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { FileSystem,
|
|
2
|
-
import {
|
|
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.
|
|
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
|
});
|
package/dist/utils/packages.js
CHANGED
|
@@ -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(
|
|
7
|
+
const StringRecord = Schema.Record(Schema.String, Schema.String);
|
|
7
8
|
const PackageJson = Schema.Struct({
|
|
8
|
-
dependencies: Schema.
|
|
9
|
-
devDependencies: Schema.
|
|
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.
|
|
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
|
-
|
|
27
|
-
yield* Command.exitCode(installDeps);
|
|
51
|
+
yield* runCommand(packageManager, [...installArgs, 'foldkit', ...exampleDeps.dependencies], projectPath);
|
|
28
52
|
const installDevArgs = getInstallArgs(packageManager, true);
|
|
29
|
-
|
|
30
|
-
|
|
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.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Create Foldkit applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -12,15 +12,11 @@
|
|
|
12
12
|
"templates"
|
|
13
13
|
],
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@effect/
|
|
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": "
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"typescript": "^6.0.2"
|
|
17
|
+
"effect": "4.0.0-beta.59",
|
|
18
|
+
"rimraf": "^6.1.3",
|
|
19
|
+
"typescript": "^6.0.3"
|
|
24
20
|
},
|
|
25
21
|
"keywords": [
|
|
26
22
|
"create-foldkit-app",
|
package/templates/base/AGENTS.md
CHANGED
|
@@ -76,7 +76,7 @@ const fetchWeather = (city: string) =>
|
|
|
76
76
|
// ...
|
|
77
77
|
return SucceededFetchWeather({ data })
|
|
78
78
|
}).pipe(
|
|
79
|
-
Effect.
|
|
79
|
+
Effect.catch(error =>
|
|
80
80
|
Effect.succeed(FailedFetchWeather({ error: String(error) })),
|
|
81
81
|
),
|
|
82
82
|
FetchWeather,
|
|
@@ -89,31 +89,38 @@ Command definitions live where they're produced — colocated with the update fu
|
|
|
89
89
|
|
|
90
90
|
### Mount
|
|
91
91
|
|
|
92
|
-
For per-element DOM work
|
|
92
|
+
For per-element DOM work that needs the live `Element` handle (anchor positioning, portaling an overlay, attaching observers, handing the element to a third-party library), define a Mount with `Mount.define` and attach it to a view element with `OnMount`. The runtime runs the Effect when the element mounts, dispatches its result Message back through `update`, and runs the paired cleanup on unmount.
|
|
93
93
|
|
|
94
94
|
```ts
|
|
95
|
-
const
|
|
95
|
+
const CompletedPortalToBody = m('CompletedPortalToBody')
|
|
96
96
|
|
|
97
|
-
const
|
|
97
|
+
const PortalToBody = Mount.define('PortalToBody', CompletedPortalToBody)
|
|
98
98
|
|
|
99
|
-
const
|
|
99
|
+
const portalToBody = PortalToBody(element =>
|
|
100
100
|
Effect.sync(() => {
|
|
101
|
-
|
|
102
|
-
element.focus()
|
|
103
|
-
}
|
|
101
|
+
document.body.appendChild(element)
|
|
104
102
|
return {
|
|
105
|
-
message:
|
|
106
|
-
cleanup:
|
|
103
|
+
message: CompletedPortalToBody(),
|
|
104
|
+
cleanup: () => element.remove(),
|
|
107
105
|
}
|
|
108
106
|
}),
|
|
109
107
|
)
|
|
110
108
|
|
|
111
109
|
// In view:
|
|
112
|
-
|
|
110
|
+
div([Class('fixed inset-0 bg-black/50'), OnMount(portalToBody)])
|
|
113
111
|
```
|
|
114
112
|
|
|
115
113
|
Cleanup is data, paired with setup as a single value. For setup with no cleanup, pass `Function.constVoid`. The `Completed*` Message marks the lifecycle without forcing a meaningful response in update.
|
|
116
114
|
|
|
115
|
+
Two rules for Mount, both must hold:
|
|
116
|
+
|
|
117
|
+
1. **The Effect uses the element parameter.** Mount provides the live element handle, and that handle is what makes Mount distinct from Command. If your Effect doesn't read or write the element, pick a different primitive.
|
|
118
|
+
2. **The work is DOM measurement or DOM manipulation on that element.** Read its geometry, mutate its CSS, attach an observer to it, portal it, hand it to a third-party library. Anything else (network, storage, focus-on-transition, scroll lock for the page) belongs in a Command returned from `update`.
|
|
119
|
+
|
|
120
|
+
Mount Effects re-run during DevTools time-travel renders. The two rules above keep Mount work inherently replay-safe (read-only measurement, idempotent DOM mutation, paired observer attach + cleanup).
|
|
121
|
+
|
|
122
|
+
Don't reach for Mount just because the work happens to coincide with an element appearing. Check what causes the work. If a Message just dispatched (like `Opened`), the cause is the Message, not the element. Use a Command returned from `update`'s handler instead. Example: focusing a search input when its dialog opens. The cause is `Opened`, not the input's existence; return a `FocusInput` Command from the `Opened` handler.
|
|
123
|
+
|
|
117
124
|
### File Organization
|
|
118
125
|
|
|
119
126
|
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`).
|
|
@@ -154,7 +161,7 @@ If the `foldkit_*` tools aren't visible, see `@foldkit/devtools-mcp` on npm for
|
|
|
154
161
|
- 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
162
|
- 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
163
|
- 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.
|
|
164
|
+
- 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
165
|
- Use `Array.take` instead of `.slice(0, n)`.
|
|
159
166
|
- 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
167
|
|
|
@@ -167,12 +174,16 @@ const ClickedSubmit = m('ClickedSubmit')
|
|
|
167
174
|
const ChangedEmail = m('ChangedEmail', { value: S.String })
|
|
168
175
|
const CompletedNavigateInternal = m('CompletedNavigateInternal')
|
|
169
176
|
|
|
170
|
-
const Message = S.Union(
|
|
177
|
+
const Message = S.Union([
|
|
178
|
+
ClickedSubmit,
|
|
179
|
+
ChangedEmail,
|
|
180
|
+
CompletedNavigateInternal,
|
|
181
|
+
])
|
|
171
182
|
type Message = typeof Message.Type
|
|
172
183
|
```
|
|
173
184
|
|
|
174
185
|
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)
|
|
186
|
+
2. **Union + type** — `S.Union([...])` followed by `type Message = typeof Message.Type` on adjacent lines (no blank line between them)
|
|
176
187
|
|
|
177
188
|
Use `typeof ClickedSubmit` in type positions to reference a schema value's type.
|
|
178
189
|
|
|
@@ -183,8 +194,8 @@ Use `typeof ClickedSubmit` in type positions to reference a schema value's type.
|
|
|
183
194
|
- Avoid `let`. Use `const` and prefer immutable patterns.
|
|
184
195
|
- Always use braces for control flow. `if (foo) { return true }` not `if (foo) return true`.
|
|
185
196
|
- Use `is*` for boolean naming e.g. `isPlaying`, `isValid`.
|
|
186
|
-
- Don't add inline or block comments to explain code
|
|
187
|
-
- Use capitalized string literals for Schema literal types: `S.
|
|
197
|
+
- 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), TSDoc (`/** ... */`) on public exports, and `// NOTE:` comments. Reserve `// NOTE:` for behavior that would mislead a careful reader into breaking things: a timing dependency that's silent if violated, a workaround for an upstream bug, a browser quirk that costs real debugging time to rediscover. The bar is high. When in doubt, delete it.
|
|
198
|
+
- Use capitalized string literals for Schema literal types: `S.Literals(['Horizontal', 'Vertical'])` not `S.Literals(['horizontal', 'vertical'])`.
|
|
188
199
|
- Capitalize namespace imports: `import * as Command from './command'` not `import * as command from './command'`.
|
|
189
200
|
- Extract magic numbers to named constants. No raw numeric literals in logic.
|
|
190
201
|
- Never use `T[]` syntax. Always use `Array<T>` or `ReadonlyArray<T>`.
|