fimo 0.3.1 → 0.3.2-experimental.1783328062928
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 +6 -12
- package/assets/agents-starter/AGENTS.md +14 -2
- package/assets/skills/fimo/SKILL.md +1 -1
- package/assets/skills/fimo/references/content.md +14 -2
- package/assets/skills/fimo/references/setup-plain-vite.md +8 -3
- package/assets/skills/fimo/references/setup-react-router.md +13 -6
- package/assets/skills/fimo/references/ui.md +1 -1
- package/assets/skills/fimo-cli/SKILL.md +30 -18
- package/assets/skills/fimo-cli/references/account.md +4 -2
- package/assets/skills/fimo-cli/references/agent-setup.md +14 -16
- package/assets/skills/fimo-cli/references/agents.md +2 -0
- package/assets/skills/fimo-cli/references/analytics.md +2 -0
- package/assets/skills/fimo-cli/references/assets.md +2 -0
- package/assets/skills/fimo-cli/references/branches.md +2 -0
- package/assets/skills/fimo-cli/references/content.md +3 -1
- package/assets/skills/fimo-cli/references/credits.md +2 -0
- package/assets/skills/fimo-cli/references/deploy.md +10 -4
- package/assets/skills/fimo-cli/references/describe.md +2 -0
- package/assets/skills/fimo-cli/references/domains.md +2 -0
- package/assets/skills/fimo-cli/references/forms.md +2 -0
- package/assets/skills/fimo-cli/references/init.md +114 -0
- package/assets/skills/fimo-cli/references/integrations/firecrawl.md +2 -0
- package/assets/skills/fimo-cli/references/integrations.md +2 -0
- package/assets/skills/fimo-cli/references/invite.md +2 -0
- package/assets/skills/fimo-cli/references/labels.md +2 -0
- package/assets/skills/fimo-cli/references/locales.md +2 -0
- package/assets/skills/fimo-cli/references/media.md +2 -0
- package/assets/skills/fimo-cli/references/preview.md +2 -0
- package/assets/skills/fimo-cli/references/project-vars-and-secrets.md +3 -1
- package/assets/skills/fimo-cli/references/projects.md +5 -3
- package/assets/skills/fimo-cli/references/schedules.md +2 -0
- package/assets/skills/fimo-migration/SKILL.md +16 -1
- package/assets/skills/fimo-migration/references/manifest.md +40 -1
- package/assets/skills/fimo-migration/references/qa.md +21 -2
- package/assets/skills/fimo-migration/references/workflow.md +12 -1
- package/assets/skills/fimo-migration/references/wrap.md +155 -0
- package/assets/skills/fimo-studio/references/visual-editing.md +5 -3
- package/dist/build/vite/index.d.ts.map +1 -1
- package/dist/build/vite/index.js +8 -6
- package/dist/build/vite/plugins/translations.d.ts +2 -8
- package/dist/build/vite/plugins/translations.d.ts.map +1 -1
- package/dist/build/vite/plugins/translations.js +3 -121
- package/dist/cli/bundle.json +2 -2
- package/dist/cli/index.js +4547 -3255
- package/dist/runtime/app/FimoProviders.d.ts +1 -11
- package/dist/runtime/app/FimoProviders.d.ts.map +1 -1
- package/dist/runtime/app/FimoProviders.js +2 -23
- package/dist/runtime/i18n/FimoLocaleContext.d.ts.map +1 -1
- package/dist/runtime/i18n/FimoLocaleContext.js +1 -0
- package/dist/runtime/next/FimoFileLink.d.ts +17 -0
- package/dist/runtime/next/FimoFileLink.d.ts.map +1 -0
- package/dist/runtime/next/FimoFileLink.js +19 -0
- package/dist/runtime/next/FimoMediaImage.d.ts +14 -0
- package/dist/runtime/next/FimoMediaImage.d.ts.map +1 -0
- package/dist/runtime/next/FimoMediaImage.js +20 -0
- package/dist/runtime/next/FimoMediaVideo.d.ts +14 -0
- package/dist/runtime/next/FimoMediaVideo.d.ts.map +1 -0
- package/dist/runtime/next/FimoMediaVideo.js +20 -0
- package/dist/runtime/next/FimoNextProviders.d.ts +30 -0
- package/dist/runtime/next/FimoNextProviders.d.ts.map +1 -0
- package/dist/runtime/next/FimoNextProviders.js +106 -0
- package/dist/runtime/next/FimoNextScripts.d.ts +10 -0
- package/dist/runtime/next/FimoNextScripts.d.ts.map +1 -0
- package/dist/runtime/next/FimoNextScripts.js +19 -0
- package/dist/runtime/next/FimoRichTextBlock.d.ts +23 -0
- package/dist/runtime/next/FimoRichTextBlock.d.ts.map +1 -0
- package/dist/runtime/next/FimoRichTextBlock.js +22 -0
- package/dist/runtime/next/FimoText.d.ts +22 -0
- package/dist/runtime/next/FimoText.d.ts.map +1 -0
- package/dist/runtime/next/FimoText.js +20 -0
- package/dist/runtime/next/WrappedContentContext.d.ts +28 -0
- package/dist/runtime/next/WrappedContentContext.d.ts.map +1 -0
- package/dist/runtime/next/WrappedContentContext.js +30 -0
- package/dist/runtime/next/index.d.ts +14 -0
- package/dist/runtime/next/index.d.ts.map +1 -0
- package/dist/runtime/next/index.js +14 -0
- package/dist/runtime/next/server.d.ts +33 -0
- package/dist/runtime/next/server.d.ts.map +1 -0
- package/dist/runtime/next/server.js +107 -0
- package/dist/runtime/primitives/labels-hook.d.ts +33 -0
- package/dist/runtime/primitives/labels-hook.d.ts.map +1 -0
- package/dist/runtime/primitives/labels-hook.js +41 -0
- package/dist/runtime/primitives/labels-store.d.ts +12 -0
- package/dist/runtime/primitives/labels-store.d.ts.map +1 -0
- package/dist/runtime/primitives/labels-store.js +60 -0
- package/dist/runtime/primitives/labels-store.test.d.ts +2 -0
- package/dist/runtime/primitives/labels-store.test.d.ts.map +1 -0
- package/dist/runtime/primitives/labels-store.test.js +66 -0
- package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts +5 -3
- package/dist/runtime/primitives/lib/useFimoLiveValue.d.ts.map +1 -1
- package/dist/runtime/primitives/lib/useFimoLiveValue.js +8 -4
- package/dist/runtime/primitives/translations.d.ts +1 -27
- package/dist/runtime/primitives/translations.d.ts.map +1 -1
- package/dist/runtime/primitives/translations.js +7 -85
- package/dist/runtime/react/index.d.ts +24 -0
- package/dist/runtime/react/index.d.ts.map +1 -0
- package/dist/runtime/react/index.js +28 -0
- package/dist/runtime/shared/display-routes.d.ts +14 -0
- package/dist/runtime/shared/display-routes.d.ts.map +1 -0
- package/dist/runtime/shared/display-routes.js +25 -0
- package/dist/runtime/shared/fimo-config.server.d.ts.map +1 -1
- package/dist/runtime/shared/fimo-config.server.js +1 -0
- package/dist/runtime/shared/fimo-config.types.d.ts +2 -0
- package/dist/runtime/shared/fimo-config.types.d.ts.map +1 -1
- package/dist/runtime/shared/labels-fetch.server.d.ts +14 -0
- package/dist/runtime/shared/labels-fetch.server.d.ts.map +1 -0
- package/dist/runtime/shared/labels-fetch.server.js +145 -0
- package/dist/scripts/detect-framework.d.ts +12 -0
- package/dist/scripts/detect-framework.js +32 -0
- package/dist/scripts/detect-framework.test.d.ts +1 -0
- package/dist/scripts/detect-framework.test.js +39 -0
- package/dist/scripts/lib/discover-next-routes.d.ts +11 -0
- package/dist/scripts/lib/discover-next-routes.js +76 -0
- package/dist/scripts/lib/discover-next-routes.test.d.ts +1 -0
- package/dist/scripts/lib/discover-next-routes.test.js +40 -0
- package/dist/scripts/scan-wrappable.d.ts +29 -0
- package/dist/scripts/scan-wrappable.js +191 -0
- package/dist/scripts/scan-wrappable.test.d.ts +1 -0
- package/dist/scripts/scan-wrappable.test.js +48 -0
- package/dist/scripts/validate-route-metadata.js +11 -0
- package/dist/scripts/validate-schemas.d.ts +13 -0
- package/dist/scripts/validate-schemas.js +113 -0
- package/dist/scripts/validate-schemas.test.d.ts +1 -0
- package/dist/scripts/validate-schemas.test.js +132 -0
- package/package.json +29 -1
- package/release.json +2 -2
- package/scripts/postinstall.mjs +10 -54
- package/templates/react-router/package.json +1 -1
- package/assets/agents-starter/claude/settings.json +0 -19
- package/assets/skills/fimo-cli/references/create.md +0 -90
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { scanWrappableFile } from './scan-wrappable.js';
|
|
3
|
+
const scan = (code) => scanWrappableFile('/test/page.tsx', code, 'app/page.tsx');
|
|
4
|
+
describe('scanWrappableFile', () => {
|
|
5
|
+
it('finds hardcoded JSX text with its line', () => {
|
|
6
|
+
const { items } = scan(`export default function Page() {
|
|
7
|
+
return (
|
|
8
|
+
<div>
|
|
9
|
+
<h1>Ship your site in hours</h1>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}`);
|
|
13
|
+
expect(items).toEqual([{ kind: 'text', file: 'app/page.tsx', line: 4, preview: 'Ship your site in hours' }]);
|
|
14
|
+
});
|
|
15
|
+
it('skips whitespace and short fragments', () => {
|
|
16
|
+
const { items } = scan(`const x = <div> {'a'} <span>ok</span> </div>;`);
|
|
17
|
+
expect(items.filter((i) => i.kind === 'text')).toEqual([]);
|
|
18
|
+
});
|
|
19
|
+
it('finds images, videos, file links, and attribute strings', () => {
|
|
20
|
+
const { items } = scan(`const x = (
|
|
21
|
+
<div>
|
|
22
|
+
<img src="/images/hero.png" alt="Hero" />
|
|
23
|
+
<video src="/demo.mp4" />
|
|
24
|
+
<a href="/whitepaper.pdf">Download</a>
|
|
25
|
+
<input placeholder="Your work email" />
|
|
26
|
+
</div>
|
|
27
|
+
);`);
|
|
28
|
+
expect(items.map((i) => i.kind).sort()).toEqual(['attribute', 'file-link', 'image', 'text', 'video']);
|
|
29
|
+
expect(items.find((i) => i.kind === 'attribute')?.preview).toBe('placeholder: Your work email');
|
|
30
|
+
});
|
|
31
|
+
it('treats content inside Fimo wrappers as wrapped', () => {
|
|
32
|
+
const { items, wrapped } = scan(`const x = (
|
|
33
|
+
<div>
|
|
34
|
+
<FimoText id="home.hero.title">Ship your site in hours</FimoText>
|
|
35
|
+
<FimoMediaImage id="home.hero.image" src="/images/hero.png" alt="Hero" />
|
|
36
|
+
</div>
|
|
37
|
+
);`);
|
|
38
|
+
expect(items).toEqual([]);
|
|
39
|
+
expect(wrapped).toBe(2);
|
|
40
|
+
});
|
|
41
|
+
it('counts t() calls as wrapped and skips regular links', () => {
|
|
42
|
+
const { items, wrapped } = scan(`const x = <input placeholder={String(t('home.email', 'Your email'))} />;
|
|
43
|
+
const y = <a href="/about">About</a>;`);
|
|
44
|
+
expect(items.filter((i) => i.kind === 'attribute')).toEqual([]);
|
|
45
|
+
expect(items.filter((i) => i.kind === 'file-link')).toEqual([]);
|
|
46
|
+
expect(wrapped).toBe(1);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -31,6 +31,17 @@ export function validateRouteMetadata(options = {}) {
|
|
|
31
31
|
const srcDir = path.resolve(cwd, 'src');
|
|
32
32
|
const routesFilePath = path.resolve(srcDir, 'routes.ts');
|
|
33
33
|
const errors = [];
|
|
34
|
+
// Everything below the i18n checks cross-validates React Router's
|
|
35
|
+
// src/routes.ts. Adopted (wrapped) projects on another framework declare it
|
|
36
|
+
// in fimo-config.json — their routes come from framework scans, so only the
|
|
37
|
+
// framework-neutral config checks apply.
|
|
38
|
+
const earlyConfig = readJson(configFilePath);
|
|
39
|
+
const framework = typeof earlyConfig.framework === 'string' ? earlyConfig.framework : 'react-router';
|
|
40
|
+
if (framework !== 'react-router') {
|
|
41
|
+
const i18n = validateI18nConfig(earlyConfig);
|
|
42
|
+
const configRoutes = loadConfigRoutes(earlyConfig);
|
|
43
|
+
return { errors: i18n.errors, routesChecked: configRoutes.length };
|
|
44
|
+
}
|
|
34
45
|
// Parse routes from src/routes.ts (file → path mapping)
|
|
35
46
|
let fileRoutes = [];
|
|
36
47
|
if (!fs.existsSync(routesFilePath)) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface SchemaFileError {
|
|
2
|
+
rule: string;
|
|
3
|
+
message: string;
|
|
4
|
+
file: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ValidateSchemasResult {
|
|
7
|
+
errors: SchemaFileError[];
|
|
8
|
+
schemasChecked: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function validateSchemas(options?: {
|
|
11
|
+
cwd?: string;
|
|
12
|
+
}): ValidateSchemasResult;
|
|
13
|
+
export declare function validateSchemaFile(absolutePath: string, file: string): SchemaFileError[];
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { basename, join, resolve } from 'node:path';
|
|
3
|
+
import { FieldTypeSchema, RESERVED_CONTENT_FIELD_NAMES } from '@fimo/api-contracts';
|
|
4
|
+
/**
|
|
5
|
+
* Validate `src/schemas/*.json` locally against the same rules the API's
|
|
6
|
+
* schema validator enforces (field types, reserved field names, required
|
|
7
|
+
* uid/name). Catching these in `fimo validate` surfaces the real problem
|
|
8
|
+
* before a `schemas push` can fail with a less direct server error.
|
|
9
|
+
*/
|
|
10
|
+
const SCHEMAS_REL = 'src/schemas';
|
|
11
|
+
const FIELD_TYPES = new Set(FieldTypeSchema.options);
|
|
12
|
+
const RESERVED_FIELD_NAMES = new Set(RESERVED_CONTENT_FIELD_NAMES);
|
|
13
|
+
export function validateSchemas(options = {}) {
|
|
14
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
15
|
+
const schemasDir = join(cwd, SCHEMAS_REL);
|
|
16
|
+
if (!existsSync(schemasDir)) {
|
|
17
|
+
return { errors: [], schemasChecked: 0 };
|
|
18
|
+
}
|
|
19
|
+
const files = readdirSync(schemasDir)
|
|
20
|
+
.filter((f) => f.endsWith('.json'))
|
|
21
|
+
.sort();
|
|
22
|
+
const errors = [];
|
|
23
|
+
for (const fileName of files) {
|
|
24
|
+
const file = join(SCHEMAS_REL, fileName);
|
|
25
|
+
errors.push(...validateSchemaFile(join(schemasDir, fileName), file));
|
|
26
|
+
}
|
|
27
|
+
return { errors, schemasChecked: files.length };
|
|
28
|
+
}
|
|
29
|
+
export function validateSchemaFile(absolutePath, file) {
|
|
30
|
+
let schema;
|
|
31
|
+
try {
|
|
32
|
+
schema = JSON.parse(readFileSync(absolutePath, 'utf-8'));
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
rule: 'schemas/parse',
|
|
38
|
+
message: `Invalid JSON: ${error instanceof Error ? error.message : String(error)}`,
|
|
39
|
+
file,
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) {
|
|
44
|
+
return [{ rule: 'schemas/shape', message: 'Schema must be a JSON object.', file }];
|
|
45
|
+
}
|
|
46
|
+
const errors = [];
|
|
47
|
+
const { uid, name, fields } = schema;
|
|
48
|
+
const expectedUid = basename(file, '.json');
|
|
49
|
+
if (typeof uid !== 'string' || uid.length === 0) {
|
|
50
|
+
errors.push({
|
|
51
|
+
rule: 'schemas/uid',
|
|
52
|
+
message: `Missing "uid". Set it to the file name: "${expectedUid}".`,
|
|
53
|
+
file,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else if (uid !== expectedUid) {
|
|
57
|
+
errors.push({
|
|
58
|
+
rule: 'schemas/uid',
|
|
59
|
+
message: `"uid" is "${uid}" but the file is ${expectedUid}.json — they must match.`,
|
|
60
|
+
file,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
64
|
+
errors.push({
|
|
65
|
+
rule: 'schemas/name',
|
|
66
|
+
message: 'Missing "name" (the human-readable label).',
|
|
67
|
+
file,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (typeof fields !== 'object' || fields === null || Array.isArray(fields)) {
|
|
71
|
+
errors.push({
|
|
72
|
+
rule: 'schemas/fields',
|
|
73
|
+
message: 'Missing "fields" object.',
|
|
74
|
+
file,
|
|
75
|
+
});
|
|
76
|
+
return errors;
|
|
77
|
+
}
|
|
78
|
+
for (const [fieldName, definition] of Object.entries(fields)) {
|
|
79
|
+
if (RESERVED_FIELD_NAMES.has(fieldName)) {
|
|
80
|
+
errors.push({
|
|
81
|
+
rule: 'schemas/reserved-field',
|
|
82
|
+
message: `Field "${fieldName}" is reserved entry metadata — the platform manages it automatically. ` +
|
|
83
|
+
`Remove it from the schema${fieldName === 'slug' ? ' (set slug inside entry data on create instead)' : ''}.`,
|
|
84
|
+
file,
|
|
85
|
+
});
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (typeof definition !== 'object' || definition === null || Array.isArray(definition)) {
|
|
89
|
+
errors.push({
|
|
90
|
+
rule: 'schemas/field-shape',
|
|
91
|
+
message: `Field "${fieldName}" must be an object like { "type": "string" }.`,
|
|
92
|
+
file,
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const { type } = definition;
|
|
97
|
+
if (typeof type !== 'string' || !FIELD_TYPES.has(type)) {
|
|
98
|
+
errors.push({
|
|
99
|
+
rule: 'schemas/field-type',
|
|
100
|
+
message: `Field "${fieldName}" has unsupported type "${String(type)}". Allowed: ${[...FIELD_TYPES].join(', ')}.`,
|
|
101
|
+
file,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if ('i18n' in definition) {
|
|
105
|
+
errors.push({
|
|
106
|
+
rule: 'schemas/field-i18n',
|
|
107
|
+
message: `Field "${fieldName}": field-level locale settings ("i18n") are not supported.`,
|
|
108
|
+
file,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return errors;
|
|
113
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { validateSchemas } from './validate-schemas';
|
|
6
|
+
describe('validateSchemas', () => {
|
|
7
|
+
let scratch = null;
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
if (scratch) {
|
|
10
|
+
rmSync(scratch, { recursive: true, force: true });
|
|
11
|
+
scratch = null;
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
function writeProject(schemas) {
|
|
15
|
+
scratch = mkdtempSync(join(tmpdir(), 'validate-schemas-'));
|
|
16
|
+
mkdirSync(join(scratch, 'src/schemas'), { recursive: true });
|
|
17
|
+
for (const [fileName, schema] of Object.entries(schemas)) {
|
|
18
|
+
writeFileSync(join(scratch, 'src/schemas', fileName), typeof schema === 'string' ? schema : JSON.stringify(schema, null, 2));
|
|
19
|
+
}
|
|
20
|
+
return scratch;
|
|
21
|
+
}
|
|
22
|
+
it('accepts a valid schema', () => {
|
|
23
|
+
const cwd = writeProject({
|
|
24
|
+
'BlogPost.json': {
|
|
25
|
+
uid: 'BlogPost',
|
|
26
|
+
name: 'Blog Posts',
|
|
27
|
+
fields: {
|
|
28
|
+
title: { type: 'string' },
|
|
29
|
+
body: { type: 'richtext' },
|
|
30
|
+
coverImage: { type: 'media' },
|
|
31
|
+
featured: { type: 'boolean' },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
expect(validateSchemas({ cwd })).toEqual({ errors: [], schemasChecked: 1 });
|
|
36
|
+
});
|
|
37
|
+
it('returns no errors when the schemas directory does not exist', () => {
|
|
38
|
+
scratch = mkdtempSync(join(tmpdir(), 'validate-schemas-'));
|
|
39
|
+
expect(validateSchemas({ cwd: scratch })).toEqual({ errors: [], schemasChecked: 0 });
|
|
40
|
+
});
|
|
41
|
+
it('rejects reserved field names like slug', () => {
|
|
42
|
+
const cwd = writeProject({
|
|
43
|
+
'TeamMember.json': {
|
|
44
|
+
uid: 'TeamMember',
|
|
45
|
+
name: 'Team Members',
|
|
46
|
+
fields: {
|
|
47
|
+
name: { type: 'string' },
|
|
48
|
+
slug: { type: 'string' },
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
const { errors } = validateSchemas({ cwd });
|
|
53
|
+
expect(errors).toHaveLength(1);
|
|
54
|
+
expect(errors[0]).toMatchObject({ rule: 'schemas/reserved-field', file: 'src/schemas/TeamMember.json' });
|
|
55
|
+
expect(errors[0].message).toContain('"slug" is reserved');
|
|
56
|
+
expect(errors[0].message).toContain('entry data');
|
|
57
|
+
});
|
|
58
|
+
it('rejects every reserved metadata name', () => {
|
|
59
|
+
const cwd = writeProject({
|
|
60
|
+
'Bad.json': {
|
|
61
|
+
uid: 'Bad',
|
|
62
|
+
name: 'Bad',
|
|
63
|
+
fields: Object.fromEntries(['id', 'documentId', 'slug', 'locale', 'createdAt', 'updatedAt', '__fimo'].map((name) => [
|
|
64
|
+
name,
|
|
65
|
+
{ type: 'string' },
|
|
66
|
+
])),
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const { errors } = validateSchemas({ cwd });
|
|
70
|
+
expect(errors).toHaveLength(7);
|
|
71
|
+
expect(errors.every((err) => err.rule === 'schemas/reserved-field')).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it('rejects unsupported field types', () => {
|
|
74
|
+
const cwd = writeProject({
|
|
75
|
+
'Product.json': {
|
|
76
|
+
uid: 'Product',
|
|
77
|
+
name: 'Products',
|
|
78
|
+
fields: { price: { type: 'decimal' } },
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const { errors } = validateSchemas({ cwd });
|
|
82
|
+
expect(errors).toHaveLength(1);
|
|
83
|
+
expect(errors[0]).toMatchObject({ rule: 'schemas/field-type' });
|
|
84
|
+
expect(errors[0].message).toContain('"decimal"');
|
|
85
|
+
expect(errors[0].message).toContain('string, richtext, media');
|
|
86
|
+
});
|
|
87
|
+
it('rejects field-level i18n settings', () => {
|
|
88
|
+
const cwd = writeProject({
|
|
89
|
+
'Page.json': {
|
|
90
|
+
uid: 'Page',
|
|
91
|
+
name: 'Pages',
|
|
92
|
+
fields: { title: { type: 'string', i18n: true } },
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const { errors } = validateSchemas({ cwd });
|
|
96
|
+
expect(errors).toHaveLength(1);
|
|
97
|
+
expect(errors[0]).toMatchObject({ rule: 'schemas/field-i18n' });
|
|
98
|
+
});
|
|
99
|
+
it('rejects a uid that does not match the file name', () => {
|
|
100
|
+
const cwd = writeProject({
|
|
101
|
+
'TeamMember.json': {
|
|
102
|
+
uid: 'Member',
|
|
103
|
+
name: 'Team Members',
|
|
104
|
+
fields: { name: { type: 'string' } },
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
const { errors } = validateSchemas({ cwd });
|
|
108
|
+
expect(errors).toHaveLength(1);
|
|
109
|
+
expect(errors[0]).toMatchObject({ rule: 'schemas/uid' });
|
|
110
|
+
});
|
|
111
|
+
it('rejects missing uid, name, and fields', () => {
|
|
112
|
+
const cwd = writeProject({ 'Empty.json': {} });
|
|
113
|
+
const { errors } = validateSchemas({ cwd });
|
|
114
|
+
expect(errors.map((err) => err.rule)).toEqual(['schemas/uid', 'schemas/name', 'schemas/fields']);
|
|
115
|
+
});
|
|
116
|
+
it('reports invalid JSON', () => {
|
|
117
|
+
const cwd = writeProject({ 'Broken.json': '{ not json' });
|
|
118
|
+
const { errors } = validateSchemas({ cwd });
|
|
119
|
+
expect(errors).toHaveLength(1);
|
|
120
|
+
expect(errors[0]).toMatchObject({ rule: 'schemas/parse' });
|
|
121
|
+
});
|
|
122
|
+
it('validates multiple files and counts them', () => {
|
|
123
|
+
const cwd = writeProject({
|
|
124
|
+
'Good.json': { uid: 'Good', name: 'Good', fields: { title: { type: 'string' } } },
|
|
125
|
+
'Bad.json': { uid: 'Bad', name: 'Bad', fields: { slug: { type: 'string' } } },
|
|
126
|
+
});
|
|
127
|
+
const result = validateSchemas({ cwd });
|
|
128
|
+
expect(result.schemasChecked).toBe(2);
|
|
129
|
+
expect(result.errors).toHaveLength(1);
|
|
130
|
+
expect(result.errors[0].file).toBe('src/schemas/Bad.json');
|
|
131
|
+
});
|
|
132
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fimo",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2-experimental.1783328062928",
|
|
4
4
|
"description": "Fimo CLI - create, deploy, and manage Fimo projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fimo": "dist/cli/index.js"
|
|
@@ -15,10 +15,23 @@
|
|
|
15
15
|
],
|
|
16
16
|
"type": "module",
|
|
17
17
|
"exports": {
|
|
18
|
+
"./package.json": "./package.json",
|
|
18
19
|
"./ui": {
|
|
19
20
|
"types": "./dist/runtime/index.d.ts",
|
|
20
21
|
"import": "./dist/runtime/index.js"
|
|
21
22
|
},
|
|
23
|
+
"./react": {
|
|
24
|
+
"types": "./dist/runtime/react/index.d.ts",
|
|
25
|
+
"import": "./dist/runtime/react/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./next": {
|
|
28
|
+
"types": "./dist/runtime/next/index.d.ts",
|
|
29
|
+
"import": "./dist/runtime/next/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./next/server": {
|
|
32
|
+
"types": "./dist/runtime/next/server.d.ts",
|
|
33
|
+
"import": "./dist/runtime/next/server.js"
|
|
34
|
+
},
|
|
22
35
|
"./react-router": {
|
|
23
36
|
"types": "./dist/runtime/react-router/index.d.ts",
|
|
24
37
|
"import": "./dist/runtime/react-router/index.js"
|
|
@@ -96,11 +109,26 @@
|
|
|
96
109
|
},
|
|
97
110
|
"peerDependencies": {
|
|
98
111
|
"@tanstack/react-query": "^5.80.6",
|
|
112
|
+
"next": ">=14.0.0",
|
|
99
113
|
"react": "^19.0.0",
|
|
100
114
|
"react-dom": "^19.0.0",
|
|
101
115
|
"react-router": "^7.0.0",
|
|
102
116
|
"vite": "^7.0.0 || ^8.0.0"
|
|
103
117
|
},
|
|
118
|
+
"peerDependenciesMeta": {
|
|
119
|
+
"@tanstack/react-query": {
|
|
120
|
+
"optional": true
|
|
121
|
+
},
|
|
122
|
+
"next": {
|
|
123
|
+
"optional": true
|
|
124
|
+
},
|
|
125
|
+
"react-router": {
|
|
126
|
+
"optional": true
|
|
127
|
+
},
|
|
128
|
+
"vite": {
|
|
129
|
+
"optional": true
|
|
130
|
+
}
|
|
131
|
+
},
|
|
104
132
|
"engines": {
|
|
105
133
|
"node": ">=20.12.0"
|
|
106
134
|
}
|
package/release.json
CHANGED
package/scripts/postinstall.mjs
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// Two paths:
|
|
3
3
|
//
|
|
4
4
|
// 1) GLOBAL install (`npm i -g fimo`, `pnpm add -g fimo`, etc.)
|
|
5
|
-
// →
|
|
6
|
-
//
|
|
5
|
+
// → print a short, non-blocking hint. We steer users toward
|
|
6
|
+
// `npx fimo@latest` (always current) and `fimo init`, and don't run any
|
|
7
|
+
// interactive prompt from a package lifecycle script.
|
|
7
8
|
//
|
|
8
9
|
// 2) LOCAL / project install (`pnpm install` in a project that has `fimo`
|
|
9
10
|
// in its dependencies)
|
|
@@ -17,20 +18,18 @@
|
|
|
17
18
|
// never break the user's install).
|
|
18
19
|
//
|
|
19
20
|
// pnpm caveat: by default, pnpm strips build scripts from non-approved deps.
|
|
20
|
-
// Fimo-controlled installs (`fimo
|
|
21
|
+
// Fimo-controlled installs (`fimo init --install`, sandbox `fimopm install`,
|
|
21
22
|
// and E2E helper installs) pass a scoped allow-all-builds flag for pnpm so the
|
|
22
23
|
// fimo postinstall hook can refresh project skills. The template intentionally
|
|
23
24
|
// does not persist allowlists in package.json / pnpm-workspace.yaml because
|
|
24
25
|
// those conflict with sandbox install flags and drift when deps change. npm /
|
|
25
26
|
// yarn run lifecycle scripts by default and need no extra config.
|
|
26
27
|
|
|
27
|
-
import { spawnSync } from 'node:child_process';
|
|
28
28
|
import fs from 'node:fs';
|
|
29
29
|
import path from 'node:path';
|
|
30
|
-
import readline from 'node:readline/promises';
|
|
31
30
|
import { fileURLToPath } from 'node:url';
|
|
32
31
|
|
|
33
|
-
import { isGlobalInstall
|
|
32
|
+
import { isGlobalInstall } from './lib/postinstall-onboarding.mjs';
|
|
34
33
|
|
|
35
34
|
const THIS_FILE = fileURLToPath(import.meta.url);
|
|
36
35
|
const CLI_ROOT = path.resolve(path.dirname(THIS_FILE), '..');
|
|
@@ -45,22 +44,12 @@ if (isGlobalInstall(process.env)) {
|
|
|
45
44
|
console.log();
|
|
46
45
|
console.log(`${green('✓')} ${bold('fimo')} installed`);
|
|
47
46
|
console.log();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const shouldInstallSkills = await confirm(`Install Fimo skills for your AI coding tools now? ${dim('[Y/n]')} `);
|
|
51
|
-
if (shouldInstallSkills) {
|
|
52
|
-
await runSkillsInstall();
|
|
53
|
-
} else {
|
|
54
|
-
console.log();
|
|
55
|
-
console.log(` Later: run ${cyan('fimo skills install')} to register Fimo skills.`);
|
|
56
|
-
}
|
|
57
|
-
} else {
|
|
58
|
-
console.log(` Tip: run ${cyan('fimo skills install')} to register Fimo skills`);
|
|
59
|
-
console.log(` for your AI coding tools (Claude Code, Cursor, Codex, ...).`);
|
|
60
|
-
console.log(` ${dim('Skip if you don’t use AI tools.')}`);
|
|
61
|
-
}
|
|
47
|
+
console.log(` Tip: run the latest without a global install:`);
|
|
48
|
+
console.log(` ${cyan('npx fimo@latest <command>')}`);
|
|
62
49
|
console.log();
|
|
63
|
-
console.log(`
|
|
50
|
+
console.log(` Get started:`);
|
|
51
|
+
console.log(` ${cyan('fimo init')} ${dim('# sign in, create or link a project')}`);
|
|
52
|
+
console.log(` ${cyan('fimo skills install')} ${dim('# set up skills for your AI coding tools')}`);
|
|
64
53
|
console.log();
|
|
65
54
|
process.exit(0);
|
|
66
55
|
}
|
|
@@ -97,36 +86,3 @@ try {
|
|
|
97
86
|
console.error('[fimo postinstall] sync failed:', err);
|
|
98
87
|
}
|
|
99
88
|
}
|
|
100
|
-
|
|
101
|
-
async function confirm(message) {
|
|
102
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
103
|
-
try {
|
|
104
|
-
const answer = await rl.question(message);
|
|
105
|
-
return parseYesNo(answer, true);
|
|
106
|
-
} finally {
|
|
107
|
-
rl.close();
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function runSkillsInstall() {
|
|
112
|
-
const entry = path.join(CLI_ROOT, 'dist', 'cli', 'index.js');
|
|
113
|
-
if (!fs.existsSync(entry)) {
|
|
114
|
-
console.log();
|
|
115
|
-
console.log(` Run ${cyan('fimo skills install')} after installation finishes.`);
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
console.log();
|
|
120
|
-
const result = spawnSync(process.execPath, [entry, 'skills', 'install', '--yes'], {
|
|
121
|
-
stdio: 'inherit',
|
|
122
|
-
env: {
|
|
123
|
-
...process.env,
|
|
124
|
-
FIMO_POSTINSTALL_CHILD: '1',
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
if (result.error || result.status !== 0) {
|
|
129
|
-
console.log();
|
|
130
|
-
console.log(` Skills were not installed. Run ${cyan('fimo skills install')} to try again.`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(fimo:*)",
|
|
5
|
-
"Bash(npx fimo:*)",
|
|
6
|
-
"Bash(pnpm:*)",
|
|
7
|
-
"Bash(pnpm dlx:*)",
|
|
8
|
-
"Bash(npx:*)",
|
|
9
|
-
"Bash(git status)",
|
|
10
|
-
"Bash(git status:*)",
|
|
11
|
-
"Bash(git diff)",
|
|
12
|
-
"Bash(git diff:*)",
|
|
13
|
-
"Bash(git log:*)",
|
|
14
|
-
"Bash(git add:*)",
|
|
15
|
-
"Bash(git restore:*)",
|
|
16
|
-
"Bash(mkdir:*)"
|
|
17
|
-
]
|
|
18
|
-
}
|
|
19
|
-
}
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# `fimo create`
|
|
2
|
-
|
|
3
|
-
Scaffold a new Fimo project linked to a remote on the Fimo platform.
|
|
4
|
-
|
|
5
|
-
## Command
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
fimo create [dir] [options]
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
| Flag | Required | Purpose |
|
|
12
|
-
| ---------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
13
|
-
| `[dir]` positional | yes in non-TTY | Target directory (e.g. `my-site`) |
|
|
14
|
-
| `--org <id>` | yes in non-TTY | Target organization **id** (use the `ID` column from `fimo orgs list`, NOT the slug). Picker shown interactively if omitted and TTY. |
|
|
15
|
-
| `--install` / `--no-install` | yes in non-TTY | Run `pnpm install` after scaffolding (or skip it). |
|
|
16
|
-
| `--agents <list>` | yes in non-TTY | Comma-separated agent IDs (e.g. `claude-code,cursor`). Pass `none` to write only universal AGENTS.md. |
|
|
17
|
-
| `-a, --agent <id>` | alternative | Repeatable form: `-a claude-code -a cursor`. |
|
|
18
|
-
| `--template-root <path>` | no | Override the bundled template (development only). |
|
|
19
|
-
|
|
20
|
-
Without the required flags in a non-TTY shell, the command exits with `Cannot prompt in a non-interactive shell.`
|
|
21
|
-
|
|
22
|
-
For `--agents`, pass the AI tool you're running in: `--agents=claude-code` for Claude Code, `--agents=cursor` for Cursor, `--agents=codex` for Codex, etc. When the flag is omitted, env-var detection of the invoking harness (e.g. `CLAUDECODE` / `CLAUDE_CODE_ENTRYPOINT` → claude-code) auto-targets that agent; other agents must be selected explicitly via `--agents`/`-a` or the interactive picker.
|
|
23
|
-
|
|
24
|
-
## What gets written
|
|
25
|
-
|
|
26
|
-
`fimo create` scaffolds:
|
|
27
|
-
|
|
28
|
-
- The project template (Vite + React 19 + TypeScript + Tailwind v4 + shadcn/ui + react-router)
|
|
29
|
-
- `.fimo.settings.json` — links this directory to the remote project (read-only after creation)
|
|
30
|
-
- `.env` — initial env vars (currently just `VITE_API_URL`)
|
|
31
|
-
- `AGENTS.md` — project rules read by all agentskills.io-compatible AI tools (Codex, OpenCode, Aider, etc.)
|
|
32
|
-
- Per-agent variants based on `--agents`:
|
|
33
|
-
- `claude-code` → `CLAUDE.md` + `.claude/settings.json`
|
|
34
|
-
- `cursor` → `.cursor/rules/fimo.mdc`
|
|
35
|
-
- `windsurf` → `.windsurfrules`
|
|
36
|
-
- `gemini-cli` → `GEMINI.md`
|
|
37
|
-
- codex / opencode / aider / others → no extra file; AGENTS.md is read natively
|
|
38
|
-
- The project-scope `fimo` skill (npm package surface: schemas, forms, `t()`, `fimo/ui` components) copied into each selected agent's skill dir (`.claude/skills/fimo/`, `.cursor/skills/fimo/`, etc.). Version-locked to the bundled `fimo` package — the installed `SKILL.md` is stamped with `fimoVersion`. Refresh later with `fimo skills sync`. See `references/agent-setup.md`.
|
|
39
|
-
- A git repository with `fimo` as the remote, an initial commit, and an initial push
|
|
40
|
-
|
|
41
|
-
Codex, OpenCode, Aider, and similar AGENTS.md-native tools work directly off AGENTS.md without a per-agent variant.
|
|
42
|
-
|
|
43
|
-
`.env` is only the bootstrap app env so local `pnpm dev` works immediately. User-managed public variables are stored with `fimo vars set` and synced into a managed `.env.local` block; hidden values use `fimo secrets set`. See `references/project-vars-and-secrets.md`.
|
|
44
|
-
|
|
45
|
-
## Right after scaffolding
|
|
46
|
-
|
|
47
|
-
Set a human-readable display name with `fimo rename "<Name>"`. Infer it from what the user is building (e.g. "Acme Coffee" for a café site, "Jane Doe Portfolio" for a personal site). The default post-`fimo create` name is an auto-generated slug; renaming updates the dashboard label and the SEO `siteName`.
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
cd my-site
|
|
51
|
-
fimo rename "Acme Coffee"
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
**Then kick off a warm-up deploy in the background** so the sandbox provisions and installs the user's deps (which may differ from the template's baked ones) while you generate code. The first deploy of a project's life pays a ~15-30s provisioning cost; doing it in the background means the user's first interactive deploy lands on a warm sandbox and finishes in ~4-10s instead. See `references/deploy.md` § Warm the sandbox early in a session.
|
|
55
|
-
|
|
56
|
-
```bash
|
|
57
|
-
fimo deploy -m "chore: init" &
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
`-m` is the commit message for any files `validate` may codegen — use a sensible conventional-commit string (e.g. `chore: init`, `chore: scaffold`).
|
|
61
|
-
|
|
62
|
-
## Load the project context
|
|
63
|
-
|
|
64
|
-
After `fimo create` completes, the project root has:
|
|
65
|
-
|
|
66
|
-
- `AGENTS.md` (universal) plus the per-agent variant for the AI tool you're running in (e.g. `CLAUDE.md` for Claude Code, `.cursor/rules/fimo.mdc` for Cursor)
|
|
67
|
-
- `.claude/skills/fimo/SKILL.md` (or the equivalent per-agent path) — the project-scope `fimo` skill, version-locked to the installed `fimo` package
|
|
68
|
-
|
|
69
|
-
**These files came into existence mid-session, so they may not be auto-loaded by the harness.** Explicitly:
|
|
70
|
-
|
|
71
|
-
1. `cd <project-dir>` — switch into the new project.
|
|
72
|
-
2. `Read` the project rules file: `AGENTS.md` (or `CLAUDE.md` if you're Claude Code).
|
|
73
|
-
3. `Read` the project skill: `.claude/skills/fimo/SKILL.md` (or `.cursor/skills/fimo/SKILL.md` for Cursor, etc.).
|
|
74
|
-
4. From the project skill's "load these references up front" section, batch-read `references/content.md`, `references/forms.md`, `references/assets.md`, `references/labels.md`, `references/locales.md`, `references/ui.md` before generating any significant code.
|
|
75
|
-
|
|
76
|
-
Once those are in context, generation respects the project's conventions (schemas, `t()`, `fimo/ui` components, asset handling).
|
|
77
|
-
|
|
78
|
-
## Adding another AI tool to an existing project later
|
|
79
|
-
|
|
80
|
-
If a teammate joins on a different AI tool (e.g. Cursor) after the project was scaffolded for claude-code only:
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
fimo skills install --agents=cursor # one command — global skill + project skill + rule file
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Run from inside the project. The command detects the project context and wires up everything for the new agent in a single shot. See `references/agent-setup.md`.
|
|
87
|
-
|
|
88
|
-
## Multi-org accounts
|
|
89
|
-
|
|
90
|
-
If you belong to a single organization, `fimo create` defaults to it. If you belong to multiple and don't pass `--org`, the CLI shows an interactive picker. For `fimo create`, pass the org **id** (from the `ID` column of `fimo orgs list`) — not the slug. Use `fimo orgs switch --org <id-or-slug>` to set a default for future `fimo create` invocations. See `references/account.md`.
|