create-foldkit-app 0.6.2 → 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 +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 +3 -7
- package/templates/base/AGENTS.md +36 -5
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.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/
|
|
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
|
-
"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
|
},
|
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,
|
|
@@ -87,6 +87,33 @@ Commands catch all errors and return Messages — side effects never crash the a
|
|
|
87
87
|
|
|
88
88
|
Command definitions live where they're produced — colocated with the update function that returns them. Don't centralize all definitions in one file.
|
|
89
89
|
|
|
90
|
+
### Mount
|
|
91
|
+
|
|
92
|
+
For per-element DOM work — focusing an input, handing the live `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
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const CompletedFocusInput = m('CompletedFocusInput')
|
|
96
|
+
|
|
97
|
+
const FocusInput = Mount.define('FocusInput', CompletedFocusInput)
|
|
98
|
+
|
|
99
|
+
const focusInput = FocusInput(element =>
|
|
100
|
+
Effect.sync(() => {
|
|
101
|
+
if (element instanceof HTMLInputElement) {
|
|
102
|
+
element.focus()
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
message: CompletedFocusInput(),
|
|
106
|
+
cleanup: Function.constVoid,
|
|
107
|
+
}
|
|
108
|
+
}),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// In view:
|
|
112
|
+
input([Type('search'), OnMount(focusInput)])
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
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
|
+
|
|
90
117
|
### File Organization
|
|
91
118
|
|
|
92
119
|
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`).
|
|
@@ -127,7 +154,7 @@ If the `foldkit_*` tools aren't visible, see `@foldkit/devtools-mcp` on npm for
|
|
|
127
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.
|
|
128
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.
|
|
129
156
|
- Never cast Schema values with `as Type`. Use callable constructors: `LoginSucceeded({ sessionId })` not `{ _tag: 'LoginSucceeded', sessionId } as Message`.
|
|
130
|
-
- Use `Option` for model fields that may be absent — not empty strings or zero values. `loginError: S.
|
|
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.
|
|
131
158
|
- Use `Array.take` instead of `.slice(0, n)`.
|
|
132
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`.
|
|
133
160
|
|
|
@@ -140,12 +167,16 @@ const ClickedSubmit = m('ClickedSubmit')
|
|
|
140
167
|
const ChangedEmail = m('ChangedEmail', { value: S.String })
|
|
141
168
|
const CompletedNavigateInternal = m('CompletedNavigateInternal')
|
|
142
169
|
|
|
143
|
-
const Message = S.Union(
|
|
170
|
+
const Message = S.Union([
|
|
171
|
+
ClickedSubmit,
|
|
172
|
+
ChangedEmail,
|
|
173
|
+
CompletedNavigateInternal,
|
|
174
|
+
])
|
|
144
175
|
type Message = typeof Message.Type
|
|
145
176
|
```
|
|
146
177
|
|
|
147
178
|
1. **Values** — all `m()` declarations, no blank lines between them
|
|
148
|
-
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)
|
|
149
180
|
|
|
150
181
|
Use `typeof ClickedSubmit` in type positions to reference a schema value's type.
|
|
151
182
|
|
|
@@ -157,7 +188,7 @@ Use `typeof ClickedSubmit` in type positions to reference a schema value's type.
|
|
|
157
188
|
- Always use braces for control flow. `if (foo) { return true }` not `if (foo) return true`.
|
|
158
189
|
- Use `is*` for boolean naming e.g. `isPlaying`, `isValid`.
|
|
159
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.
|
|
160
|
-
- Use capitalized string literals for Schema literal types: `S.
|
|
191
|
+
- Use capitalized string literals for Schema literal types: `S.Literals(['Horizontal', 'Vertical'])` not `S.Literals(['horizontal', 'vertical'])`.
|
|
161
192
|
- Capitalize namespace imports: `import * as Command from './command'` not `import * as command from './command'`.
|
|
162
193
|
- Extract magic numbers to named constants. No raw numeric literals in logic.
|
|
163
194
|
- Never use `T[]` syntax. Always use `Array<T>` or `ReadonlyArray<T>`.
|