fimo 0.2.3-staging.10 → 0.2.3-staging.7
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/assets/skills/fimo/references/setup-plain-vite.md +1 -4
- package/assets/skills/fimo/references/setup-react-router.md +2 -3
- package/assets/skills/fimo-cli/references/agent-setup.md +1 -1
- package/assets/skills/fimo-cli/references/branches.md +0 -2
- package/assets/skills/fimo-studio/references/code-editor.md +1 -1
- package/dist/cli/bundle.json +2 -2
- package/dist/cli/index.js +631 -754
- package/dist/runtime/templates.d.ts +5 -5
- package/dist/runtime/templates.d.ts.map +1 -1
- package/dist/runtime/templates.js +13 -14
- package/package.json +2 -1
- package/scripts/lib/cleanup-release.mjs +64 -0
- package/scripts/lib/cleanup-release.test.ts +142 -0
- package/scripts/postinstall.mjs +4 -6
- package/scripts/publish-npm.mjs +1 -12
- package/scripts/publish-tarball.mjs +245 -0
- package/templates/react-router/package.json +8 -1
- package/templates/react-router/pnpm-workspace.yaml +3 -6
- package/templates/react-router/src/index.css +4 -0
- package/templates/react-router/src/pages/Index.tsx +97 -38
- package/scripts/lib/dev-release-state.mjs +0 -44
- package/scripts/lib/dev-release-state.test.ts +0 -36
- package/templates/react-router/public/claude-color.svg +0 -1
- package/templates/react-router/public/codex.svg +0 -1
- package/templates/react-router/public/copilot-color.svg +0 -1
- package/templates/react-router/public/cursor-light.svg +0 -12
- package/templates/react-router/public/fimo-logo-black.svg +0 -3
|
@@ -30,7 +30,8 @@ export declare function getTemplatePath(framework?: TemplateFramework): string;
|
|
|
30
30
|
*/
|
|
31
31
|
export declare function getLegacyTemplateDir(): string;
|
|
32
32
|
export interface ReleaseMeta {
|
|
33
|
-
|
|
33
|
+
id: string | null;
|
|
34
|
+
source: 'npm' | 'url' | 'dev' | null;
|
|
34
35
|
}
|
|
35
36
|
export declare function readReleaseMeta(): ReleaseMeta;
|
|
36
37
|
export declare function readCliSemver(): string | null;
|
|
@@ -50,10 +51,9 @@ interface PkgWithDeps {
|
|
|
50
51
|
devDependencies?: Record<string, string>;
|
|
51
52
|
}
|
|
52
53
|
/**
|
|
53
|
-
* Rewrite `fimo` (in dependencies + devDependencies) to the current pin.
|
|
54
|
-
* touches entries that look like a
|
|
55
|
-
*
|
|
56
|
-
* values are left alone.
|
|
54
|
+
* Rewrite `fimo` (in dependencies + devDependencies) to the current pin.
|
|
55
|
+
* Only touches entries that look like a fimo tarball URL or a semver range —
|
|
56
|
+
* never clobbers a bespoke value the user might have set.
|
|
57
57
|
*
|
|
58
58
|
* Returns true if anything changed.
|
|
59
59
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/runtime/templates.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/runtime/templates.ts"],"names":[],"mappings":"AAiCA,eAAO,MAAM,mBAAmB,oDAAqD,CAAC;AACtF,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,mBAAmB,CAAC,CAAC,MAAM,CAAC,CAAC;AACrE,eAAO,MAAM,gBAAgB,EAAE,iBAAkC,CAAC;AAElE;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,SAAS,GAAE,iBAAoC,GAAG,MAAM,CAEvF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAM7C;AAsBD,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,MAAM,EAAE,KAAK,GAAG,KAAK,GAAG,KAAK,GAAG,IAAI,CAAC;CACtC;AAED,wBAAgB,eAAe,IAAI,WAAW,CAU7C;AAED,wBAAgB,aAAa,IAAI,MAAM,GAAG,IAAI,CAW7C;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,WAAW,EACjB,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,OAAO,GAAE;IAAE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAC/C,MAAM,GAAG,IAAI,CAaf;AAED,uDAAuD;AACvD,wBAAgB,cAAc,CAAC,OAAO,GAAE;IAAE,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAAG,MAAM,GAAG,IAAI,CAE9F;AAED,UAAU,WAAW;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,GAAG,GAAE,MAAM,GAAG,IAAuB,GAAG,OAAO,CAsB/F"}
|
|
@@ -27,6 +27,7 @@ const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
27
27
|
const PACKAGE_ROOT = path.resolve(MODULE_DIR, '..', '..');
|
|
28
28
|
const RELEASE_FILE = path.join(PACKAGE_ROOT, 'release.json');
|
|
29
29
|
const PACKAGE_JSON_FILE = path.join(PACKAGE_ROOT, 'package.json');
|
|
30
|
+
const TARBALL_URL_BASE = 'https://pub-41cdea46386f4b238d8c528c4327dfc1.r2.dev/cli';
|
|
30
31
|
export const TEMPLATE_FRAMEWORKS = ['react-router', 'astro', 'next', 'vite'];
|
|
31
32
|
export const DEFAULT_TEMPLATE = 'react-router';
|
|
32
33
|
/**
|
|
@@ -69,11 +70,12 @@ export function readReleaseMeta() {
|
|
|
69
70
|
try {
|
|
70
71
|
const raw = fs.readFileSync(RELEASE_FILE, 'utf8');
|
|
71
72
|
const parsed = JSON.parse(raw);
|
|
72
|
-
const
|
|
73
|
-
|
|
73
|
+
const id = typeof parsed.id === 'string' && parsed.id.length > 0 ? parsed.id : null;
|
|
74
|
+
const source = parsed.source === 'npm' || parsed.source === 'url' || parsed.source === 'dev' ? parsed.source : null;
|
|
75
|
+
return { id, source };
|
|
74
76
|
}
|
|
75
77
|
catch {
|
|
76
|
-
return { source: null };
|
|
78
|
+
return { id: null, source: null };
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
81
|
export function readCliSemver() {
|
|
@@ -99,6 +101,9 @@ export function computeFimoPin(meta, semver, options = {}) {
|
|
|
99
101
|
return semver;
|
|
100
102
|
}
|
|
101
103
|
}
|
|
104
|
+
if (meta.id) {
|
|
105
|
+
return `${TARBALL_URL_BASE}/fimo-${meta.id}.tgz`;
|
|
106
|
+
}
|
|
102
107
|
if ((meta.source === 'dev' || meta.source === null) && options.devPackageRoot) {
|
|
103
108
|
return `file:${options.devPackageRoot}`;
|
|
104
109
|
}
|
|
@@ -109,10 +114,9 @@ export function resolveFimoPin(options = {}) {
|
|
|
109
114
|
return computeFimoPin(readReleaseMeta(), readCliSemver(), options);
|
|
110
115
|
}
|
|
111
116
|
/**
|
|
112
|
-
* Rewrite `fimo` (in dependencies + devDependencies) to the current pin.
|
|
113
|
-
* touches entries that look like a
|
|
114
|
-
*
|
|
115
|
-
* values are left alone.
|
|
117
|
+
* Rewrite `fimo` (in dependencies + devDependencies) to the current pin.
|
|
118
|
+
* Only touches entries that look like a fimo tarball URL or a semver range —
|
|
119
|
+
* never clobbers a bespoke value the user might have set.
|
|
116
120
|
*
|
|
117
121
|
* Returns true if anything changed.
|
|
118
122
|
*/
|
|
@@ -130,14 +134,9 @@ export function rewriteFimoDep(pkg, pin = resolveFimoPin()) {
|
|
|
130
134
|
if (typeof current !== 'string') {
|
|
131
135
|
continue;
|
|
132
136
|
}
|
|
133
|
-
|
|
134
|
-
// rewrite the committed template's dist-tag, or (dev flow) swap a local
|
|
135
|
-
// `file:` build for a published version on deploy.
|
|
136
|
-
const looksLikeLegacyR2 = current.includes('r2.dev/cli/fimo-');
|
|
137
|
-
const looksLikeLocalFile = current.startsWith('file:');
|
|
137
|
+
const looksLikeFimoTarball = current.includes(`${TARBALL_URL_BASE}/fimo-`);
|
|
138
138
|
const looksLikeSemver = /^[\^~]?\d+\.\d+\.\d+/.test(current);
|
|
139
|
-
|
|
140
|
-
if ((looksLikeLegacyR2 || looksLikeLocalFile || looksLikeSemver || looksLikeOwnedDistTag) && current !== pin) {
|
|
139
|
+
if ((looksLikeFimoTarball || looksLikeSemver) && current !== pin) {
|
|
141
140
|
deps['fimo'] = pin;
|
|
142
141
|
changed = true;
|
|
143
142
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fimo",
|
|
3
|
-
"version": "0.2.3-staging.
|
|
3
|
+
"version": "0.2.3-staging.7",
|
|
4
4
|
"description": "Fimo CLI - create, deploy, and manage Fimo projects",
|
|
5
5
|
"bin": {
|
|
6
6
|
"fimo": "dist/cli/index.js"
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"check:types": "tsc -b tsconfig.json",
|
|
46
46
|
"start": "tsx src/cli/index.ts",
|
|
47
47
|
"install-cli": "pnpm build && node scripts/install-cli.mjs",
|
|
48
|
+
"publish-cli": "node scripts/publish-tarball.mjs",
|
|
48
49
|
"publish:npm": "node scripts/publish-npm.mjs",
|
|
49
50
|
"prepublishOnly": "pnpm run build && pnpm run build:bundle",
|
|
50
51
|
"release:bump": "node scripts/bump-version.mjs",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { rmSync } from 'node:fs';
|
|
2
|
+
|
|
3
|
+
// Helpers around the workspace-`release.json` cleanup behaviour used by
|
|
4
|
+
// `publish-tarball.mjs`. Extracted so the cleanup logic can be unit
|
|
5
|
+
// tested without spinning up the full clean → build → bundle → pack →
|
|
6
|
+
// upload chain.
|
|
7
|
+
//
|
|
8
|
+
// The contract is small but worth pinning:
|
|
9
|
+
// 1. `cleanupReleaseFile` is idempotent and safe when the file is
|
|
10
|
+
// already absent.
|
|
11
|
+
// 2. `registerReleaseCleanup` wires cleanup to process.on('exit'),
|
|
12
|
+
// SIGINT, and SIGTERM so the file is removed on any termination
|
|
13
|
+
// path (including process.exit calls inside the publish script).
|
|
14
|
+
// 3. The `skip` option short-circuits both surfaces - `cleanupReleaseFile`
|
|
15
|
+
// checks it, and `registerReleaseCleanup` closes over it.
|
|
16
|
+
|
|
17
|
+
export function cleanupReleaseFile(releaseFile, options = {}) {
|
|
18
|
+
if (options.skip) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
rmSync(releaseFile, { force: true });
|
|
23
|
+
return true;
|
|
24
|
+
} catch {
|
|
25
|
+
// Best effort. A stale release.json is annoying but not
|
|
26
|
+
// worth bailing the publish for.
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Registers cleanup against the Node lifecycle and returns a callable
|
|
32
|
+
// that does the cleanup once-only. Useful in two ways:
|
|
33
|
+
// a) The bottom of the publish script calls the returned function
|
|
34
|
+
// explicitly so the success path can log a confirmation line.
|
|
35
|
+
// b) Failure paths (process.exit / SIGINT / SIGTERM) hit the
|
|
36
|
+
// registered handlers and still run the cleanup.
|
|
37
|
+
//
|
|
38
|
+
// Idempotency is enforced via a closure flag - calling the returned
|
|
39
|
+
// function twice (e.g., once explicitly + once on exit) only removes
|
|
40
|
+
// the file the first time.
|
|
41
|
+
export function registerReleaseCleanup(releaseFile, options = {}) {
|
|
42
|
+
let alreadyCleaned = false;
|
|
43
|
+
const skip = options.skip === true;
|
|
44
|
+
|
|
45
|
+
const cleanupOnce = () => {
|
|
46
|
+
if (alreadyCleaned || skip) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
alreadyCleaned = true;
|
|
50
|
+
return cleanupReleaseFile(releaseFile);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
process.on('exit', cleanupOnce);
|
|
54
|
+
process.on('SIGINT', () => {
|
|
55
|
+
cleanupOnce();
|
|
56
|
+
process.exit(130);
|
|
57
|
+
});
|
|
58
|
+
process.on('SIGTERM', () => {
|
|
59
|
+
cleanupOnce();
|
|
60
|
+
process.exit(143);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return cleanupOnce;
|
|
64
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
8
|
+
|
|
9
|
+
import { cleanupReleaseFile, registerReleaseCleanup } from './cleanup-release.mjs';
|
|
10
|
+
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const CLEANUP_MJS = path.resolve(__dirname, 'cleanup-release.mjs');
|
|
13
|
+
|
|
14
|
+
let dir: string;
|
|
15
|
+
let releaseFile: string;
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
dir = mkdtempSync(path.join(tmpdir(), 'fimo-cli-cleanup-'));
|
|
19
|
+
releaseFile = path.join(dir, 'release.json');
|
|
20
|
+
writeFileSync(releaseFile, JSON.stringify({ id: 'fixture', source: 'url', apiUrl: 'https://api.staging.fimo.team' }));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
rmSync(dir, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('cleanupReleaseFile', () => {
|
|
28
|
+
it('removes the file', () => {
|
|
29
|
+
expect(existsSync(releaseFile)).toBe(true);
|
|
30
|
+
expect(cleanupReleaseFile(releaseFile)).toBe(true);
|
|
31
|
+
expect(existsSync(releaseFile)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('respects the skip option (file survives)', () => {
|
|
35
|
+
cleanupReleaseFile(releaseFile, { skip: true });
|
|
36
|
+
expect(existsSync(releaseFile)).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('is a no-op when the file is absent (no throw)', () => {
|
|
40
|
+
rmSync(releaseFile);
|
|
41
|
+
expect(() => cleanupReleaseFile(releaseFile)).not.toThrow();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('is idempotent (re-running on an already-removed file is fine)', () => {
|
|
45
|
+
cleanupReleaseFile(releaseFile);
|
|
46
|
+
expect(() => cleanupReleaseFile(releaseFile)).not.toThrow();
|
|
47
|
+
expect(existsSync(releaseFile)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns false when skipped', () => {
|
|
51
|
+
expect(cleanupReleaseFile(releaseFile, { skip: true })).toBe(false);
|
|
52
|
+
expect(existsSync(releaseFile)).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('registerReleaseCleanup - direct (synchronous) call', () => {
|
|
57
|
+
// These tests exercise the returned closure directly. The
|
|
58
|
+
// process.on('exit') wiring is verified by the spawn-based test
|
|
59
|
+
// below (running the registration in a child whose lifetime we
|
|
60
|
+
// can observe).
|
|
61
|
+
//
|
|
62
|
+
// We do NOT call process.on('exit') in the test process itself,
|
|
63
|
+
// since that would pollute the vitest runner and other test files.
|
|
64
|
+
// Instead, the closure returned by registerReleaseCleanup is
|
|
65
|
+
// invoked manually as it would be from `cleanupReleaseJson()` at
|
|
66
|
+
// the bottom of publish-tarball.mjs.
|
|
67
|
+
|
|
68
|
+
it('first explicit call cleans; subsequent calls are no-ops', () => {
|
|
69
|
+
const cleanup = registerReleaseCleanup(releaseFile);
|
|
70
|
+
expect(existsSync(releaseFile)).toBe(true);
|
|
71
|
+
cleanup();
|
|
72
|
+
expect(existsSync(releaseFile)).toBe(false);
|
|
73
|
+
// Re-instate the file and verify the closure doesn't clean again.
|
|
74
|
+
writeFileSync(releaseFile, 'second');
|
|
75
|
+
cleanup();
|
|
76
|
+
expect(readFileSync(releaseFile, 'utf8')).toBe('second');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('skip:true short-circuits the closure', () => {
|
|
80
|
+
const cleanup = registerReleaseCleanup(releaseFile, { skip: true });
|
|
81
|
+
cleanup();
|
|
82
|
+
expect(existsSync(releaseFile)).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('registerReleaseCleanup - process lifecycle (subprocess)', () => {
|
|
87
|
+
// The process.on('exit') wiring only fires when the host process
|
|
88
|
+
// actually exits. To verify it without exiting the test runner,
|
|
89
|
+
// we spawn a small child that registers cleanup and then returns
|
|
90
|
+
// - observing the file state from the parent after the child
|
|
91
|
+
// terminates.
|
|
92
|
+
|
|
93
|
+
function runChild(opts: { skip?: boolean; signal?: 'SIGINT' | 'SIGTERM' | null } = {}) {
|
|
94
|
+
const childScript = path.join(dir, 'child.mjs');
|
|
95
|
+
const skipFlag = opts.skip ? 'true' : 'false';
|
|
96
|
+
if (opts.signal) {
|
|
97
|
+
// Long-running child that we send a signal to. Loops to keep
|
|
98
|
+
// the process alive until the signal arrives.
|
|
99
|
+
writeFileSync(
|
|
100
|
+
childScript,
|
|
101
|
+
`import { registerReleaseCleanup } from '${CLEANUP_MJS}';
|
|
102
|
+
registerReleaseCleanup(${JSON.stringify(releaseFile)}, { skip: ${skipFlag} });
|
|
103
|
+
setInterval(() => {}, 1000);
|
|
104
|
+
`
|
|
105
|
+
);
|
|
106
|
+
const child = spawnSync(process.execPath, [childScript], {
|
|
107
|
+
timeout: 4000,
|
|
108
|
+
killSignal: opts.signal,
|
|
109
|
+
});
|
|
110
|
+
// spawnSync's timeout sends killSignal; we still get .status / .signal.
|
|
111
|
+
return child;
|
|
112
|
+
}
|
|
113
|
+
writeFileSync(
|
|
114
|
+
childScript,
|
|
115
|
+
`import { registerReleaseCleanup } from '${CLEANUP_MJS}';
|
|
116
|
+
registerReleaseCleanup(${JSON.stringify(releaseFile)}, { skip: ${skipFlag} });
|
|
117
|
+
// Exit naturally after registering - process.on('exit') should fire.
|
|
118
|
+
`
|
|
119
|
+
);
|
|
120
|
+
return execFileSync(process.execPath, [childScript], { encoding: 'utf8' });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
it('removes the file when the child exits naturally', () => {
|
|
124
|
+
runChild();
|
|
125
|
+
expect(existsSync(releaseFile)).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('preserves the file when the child registered with skip:true', () => {
|
|
129
|
+
runChild({ skip: true });
|
|
130
|
+
expect(existsSync(releaseFile)).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('removes the file when the child is killed with SIGTERM', () => {
|
|
134
|
+
runChild({ signal: 'SIGTERM' });
|
|
135
|
+
expect(existsSync(releaseFile)).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('removes the file when the child is killed with SIGINT', () => {
|
|
139
|
+
runChild({ signal: 'SIGINT' });
|
|
140
|
+
expect(existsSync(releaseFile)).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -17,12 +17,10 @@
|
|
|
17
17
|
// never break the user's install).
|
|
18
18
|
//
|
|
19
19
|
// pnpm caveat: by default, pnpm strips build scripts from non-approved deps.
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// those conflict with sandbox install flags and drift when deps change. npm /
|
|
25
|
-
// yarn run lifecycle scripts by default and need no extra config.
|
|
20
|
+
// Templates scaffolded by `fimo create` approve `fimo` and `esbuild` builds in
|
|
21
|
+
// pnpm-workspace.yaml (`allowBuilds`, pnpm 11+) and package.json
|
|
22
|
+
// (`onlyBuiltDependencies`, pnpm 10-era installs). npm / yarn run scripts by
|
|
23
|
+
// default and need no extra config.
|
|
26
24
|
|
|
27
25
|
import fs from 'node:fs';
|
|
28
26
|
import path from 'node:path';
|
package/scripts/publish-npm.mjs
CHANGED
|
@@ -14,9 +14,7 @@
|
|
|
14
14
|
// staging URLs for experimental/staging, prod for latest. Never a tunnel.
|
|
15
15
|
// 4. For prerelease channels, synthesizes a disposable X.Y.Z-<tag>.<n> version.
|
|
16
16
|
// 5. Runs `npm publish --tag <distTag>` (no --provenance — strapi/fimo is private).
|
|
17
|
-
// 6.
|
|
18
|
-
// .fimo/dev.json for local source-checkout deploys.
|
|
19
|
-
// 7. Restores package.json + removes the transient release.json.
|
|
17
|
+
// 6. Restores package.json + removes the transient release.json.
|
|
20
18
|
//
|
|
21
19
|
// `npm publish` triggers prepublishOnly (clean → build → build:bundle), so the
|
|
22
20
|
// bundled tarball is always fresh.
|
|
@@ -27,7 +25,6 @@ import { dirname, resolve } from 'node:path';
|
|
|
27
25
|
import { fileURLToPath } from 'node:url';
|
|
28
26
|
|
|
29
27
|
import { resolveChannel } from './lib/channels.mjs';
|
|
30
|
-
import { writeDevReleaseState } from './lib/dev-release-state.mjs';
|
|
31
28
|
import {
|
|
32
29
|
computePublishVersion,
|
|
33
30
|
pinTemplateFimo,
|
|
@@ -123,14 +120,6 @@ try {
|
|
|
123
120
|
console.error(`[publish-npm] npm publish failed (exit ${result.status}).`);
|
|
124
121
|
process.exit(result.status ?? 1);
|
|
125
122
|
}
|
|
126
|
-
|
|
127
|
-
if (!dryRun && channel.distTag === 'experimental') {
|
|
128
|
-
const devStatePath = writeDevReleaseState(CLI_ROOT, {
|
|
129
|
-
version: publishVersion,
|
|
130
|
-
channel: channel.distTag,
|
|
131
|
-
});
|
|
132
|
-
console.log(`[publish-npm] wrote ${devStatePath}`);
|
|
133
|
-
}
|
|
134
123
|
} finally {
|
|
135
124
|
// Restore the workspace: package.json version, template fimo pin, and remove
|
|
136
125
|
// the transient release.json so local `fimo` runs fall back to localhost.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Builds the Fimo CLI, packs it into a tarball, and uploads the tarball to
|
|
3
|
+
// a Cloudflare R2 bucket via `wrangler r2 object put`. Meant for internal
|
|
4
|
+
// distribution ahead of a real npm publish.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// pnpm -F cli publish:tarball
|
|
8
|
+
// pnpm -F cli publish:tarball -- --bucket fimo-cli-test --remote
|
|
9
|
+
// pnpm -F cli publish:tarball -- --staging
|
|
10
|
+
// pnpm -F cli publish:tarball -- --api-url https://api.staging.fimo.team --web-url https://staging.fimo.team
|
|
11
|
+
//
|
|
12
|
+
// Env vars:
|
|
13
|
+
// FIMO_CLI_R2_BUCKET override the target R2 bucket (default: fimo-cli-test)
|
|
14
|
+
// FIMO_CLI_API_URL bake default apiUrl into the tarball (same as --api-url)
|
|
15
|
+
// FIMO_CLI_WEB_URL bake default webUrl into the tarball (same as --web-url)
|
|
16
|
+
// CLOUDFLARE_API_TOKEN, CLOUDFLARE_ACCOUNT_ID - standard wrangler auth
|
|
17
|
+
import { spawnSync } from 'node:child_process';
|
|
18
|
+
import { randomBytes } from 'node:crypto';
|
|
19
|
+
import { chmodSync, existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { dirname, resolve } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
import { registerReleaseCleanup } from './lib/cleanup-release.mjs';
|
|
24
|
+
|
|
25
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const CLI_ROOT = resolve(HERE, '..');
|
|
27
|
+
|
|
28
|
+
// Load apps/cli/.env if present, so CLOUDFLARE_* and FIMO_CLI_* vars don't have
|
|
29
|
+
// to be exported in the shell. No-op when the file doesn't exist (CI, fresh
|
|
30
|
+
// clones), so this stays safe for everyone.
|
|
31
|
+
const ENV_FILE = resolve(CLI_ROOT, '.env');
|
|
32
|
+
if (existsSync(ENV_FILE)) {
|
|
33
|
+
process.loadEnvFile(ENV_FILE);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const STAGING_API_URL = 'https://api.staging.fimo.team';
|
|
37
|
+
const STAGING_WEB_URL = 'https://staging.fimo.team';
|
|
38
|
+
|
|
39
|
+
function parseArgs(argv) {
|
|
40
|
+
const args = {
|
|
41
|
+
bucket: process.env.FIMO_CLI_R2_BUCKET || 'fimo-cli-test',
|
|
42
|
+
remote: true,
|
|
43
|
+
apiUrl: process.env.FIMO_CLI_API_URL?.trim() || null,
|
|
44
|
+
webUrl: process.env.FIMO_CLI_WEB_URL?.trim() || null,
|
|
45
|
+
keepReleaseJson: false,
|
|
46
|
+
};
|
|
47
|
+
for (let i = 0; i < argv.length; i++) {
|
|
48
|
+
const arg = argv[i];
|
|
49
|
+
if (arg === '--bucket') {
|
|
50
|
+
args.bucket = argv[++i];
|
|
51
|
+
} else if (arg === '--local') {
|
|
52
|
+
args.remote = false;
|
|
53
|
+
} else if (arg === '--remote') {
|
|
54
|
+
args.remote = true;
|
|
55
|
+
} else if (arg === '--api-url') {
|
|
56
|
+
args.apiUrl = argv[++i];
|
|
57
|
+
} else if (arg === '--web-url') {
|
|
58
|
+
args.webUrl = argv[++i];
|
|
59
|
+
} else if (arg === '--staging') {
|
|
60
|
+
args.apiUrl = args.apiUrl || STAGING_API_URL;
|
|
61
|
+
args.webUrl = args.webUrl || STAGING_WEB_URL;
|
|
62
|
+
} else if (arg === '--id') {
|
|
63
|
+
args.id = argv[++i];
|
|
64
|
+
} else if (arg === '--keep-release-json') {
|
|
65
|
+
// Escape hatch for debugging the publish flow itself. Normally
|
|
66
|
+
// we want release.json gone from the workspace so local fimo
|
|
67
|
+
// invocations (and `fimovm link --path apps/cli`) fall back to
|
|
68
|
+
// localhost rather than the URL we just baked into the tarball.
|
|
69
|
+
args.keepReleaseJson = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return args;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function run(command, args, opts = {}) {
|
|
76
|
+
console.log(`[cli/publish] $ ${command} ${args.join(' ')}`);
|
|
77
|
+
const result = spawnSync(command, args, { stdio: 'inherit', ...opts });
|
|
78
|
+
if (result.status !== 0) {
|
|
79
|
+
console.error(`[cli/publish] command failed (exit ${result.status}): ${command} ${args.join(' ')}`);
|
|
80
|
+
process.exit(result.status ?? 1);
|
|
81
|
+
}
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { bucket, remote, apiUrl, webUrl, id: customId, keepReleaseJson } = parseArgs(process.argv.slice(2));
|
|
86
|
+
|
|
87
|
+
const pkg = JSON.parse(readFileSync(resolve(CLI_ROOT, 'package.json'), 'utf8'));
|
|
88
|
+
|
|
89
|
+
// Default: generate a random release id per publish so the URL changes each
|
|
90
|
+
// time (sidesteps pnpm/CDN cache).
|
|
91
|
+
//
|
|
92
|
+
// `--id <name>` gives you a *channel* (e.g. `marc-dev`) but we ALWAYS append
|
|
93
|
+
// a short random suffix to the actual upload id (`marc-dev-abc123`). Without
|
|
94
|
+
// this, package managers (pnpm content store, npm tarball cache, Blaxel
|
|
95
|
+
// sandbox cache) key by URL and never revalidate against the origin —
|
|
96
|
+
// `Cache-Control: no-cache` only helps CF edge + HTTP intermediaries, not
|
|
97
|
+
// the local package store. SHA-suffixed URLs sidestep every cache layer.
|
|
98
|
+
//
|
|
99
|
+
// The workspace release.json (written below) captures the SHA-suffixed id,
|
|
100
|
+
// so `fimo create` pins new scaffolds to the fresh URL automatically.
|
|
101
|
+
const channelSuffix = randomBytes(3).toString('hex');
|
|
102
|
+
const releaseId = customId ? `${customId}-${channelSuffix}` : randomBytes(6).toString('hex');
|
|
103
|
+
const releaseFile = resolve(CLI_ROOT, 'release.json');
|
|
104
|
+
// `source` marks where the binary came from. R2/HTTP-hosted tarballs are
|
|
105
|
+
// "url"; npm publishes will write "source: 'npm'" from their own flow
|
|
106
|
+
// (P0-7 CI workflow). Dev / source runs have no `release.json` and resolve
|
|
107
|
+
// to defaults at runtime.
|
|
108
|
+
const releasePayload = { id: releaseId, source: 'url' };
|
|
109
|
+
if (apiUrl) {
|
|
110
|
+
releasePayload.apiUrl = apiUrl;
|
|
111
|
+
}
|
|
112
|
+
if (webUrl) {
|
|
113
|
+
releasePayload.webUrl = webUrl;
|
|
114
|
+
}
|
|
115
|
+
writeFileSync(releaseFile, JSON.stringify(releasePayload, null, 2) + '\n');
|
|
116
|
+
console.log(`[cli/publish] release id: ${releaseId}`);
|
|
117
|
+
if (apiUrl) {
|
|
118
|
+
console.log(`[cli/publish] baked apiUrl: ${apiUrl}`);
|
|
119
|
+
}
|
|
120
|
+
if (webUrl) {
|
|
121
|
+
console.log(`[cli/publish] baked webUrl: ${webUrl}`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// release.json is a publish artefact: written here, captured into the
|
|
125
|
+
// tarball by `pnpm pack`, then removed from the workspace by default.
|
|
126
|
+
// Without cleanup, the file lingers and every subsequent `fimo`
|
|
127
|
+
// invocation from the workspace (including via `fimovm link
|
|
128
|
+
// --path apps/cli`) reads the URL we just baked for someone else's
|
|
129
|
+
// tarball.
|
|
130
|
+
//
|
|
131
|
+
// EXCEPTION: when `--id <name>` is set, we KEEP the file. Dev channels
|
|
132
|
+
// want the workspace fimo to pin scaffolds to the freshly-published
|
|
133
|
+
// SHA-suffixed URL — that's the whole point of the channel. Each
|
|
134
|
+
// publish overwrites release.json with the new SHA-suffixed id, so
|
|
135
|
+
// you don't have to manually edit it between iterations.
|
|
136
|
+
//
|
|
137
|
+
// `--keep-release-json` forces keep regardless (debugging escape hatch).
|
|
138
|
+
// The cleanup runs on normal completion (explicit call at the bottom
|
|
139
|
+
// of the script), on any `process.exit(...)` from a failing `run()`
|
|
140
|
+
// step, and on SIGINT / SIGTERM. Behaviours covered by unit tests in
|
|
141
|
+
// `lib/cleanup-release.test.ts`.
|
|
142
|
+
const shouldKeep = keepReleaseJson || !!customId;
|
|
143
|
+
const cleanupReleaseJson = registerReleaseCleanup(releaseFile, { skip: shouldKeep });
|
|
144
|
+
|
|
145
|
+
const tarballName = `${pkg.name}-${pkg.version}.tgz`;
|
|
146
|
+
const tarballPath = resolve(CLI_ROOT, tarballName);
|
|
147
|
+
|
|
148
|
+
// 1. Clean previous outputs so `tsc -b` can't skip emit on stale tsbuildinfo
|
|
149
|
+
// when `dist/` has been removed out from under it.
|
|
150
|
+
run('pnpm', ['clean'], { cwd: CLI_ROOT });
|
|
151
|
+
|
|
152
|
+
// 2. Build (prebuild copies the template).
|
|
153
|
+
run('pnpm', ['build'], { cwd: CLI_ROOT });
|
|
154
|
+
|
|
155
|
+
// 3. Bundle the CLI bin into a single self-contained `dist/cli/index.js`. The
|
|
156
|
+
// subpath exports under `dist/runtime/` and `dist/build/` stay multi-file
|
|
157
|
+
// so user apps can keep importing `fimo/ui`, `fimo/vite`, etc.
|
|
158
|
+
run('pnpm', ['build:bundle'], { cwd: CLI_ROOT });
|
|
159
|
+
|
|
160
|
+
// 4. Sanity check: the bundled bin must exist before we pack and upload.
|
|
161
|
+
const binPath = resolve(CLI_ROOT, 'dist/cli/index.js');
|
|
162
|
+
if (!existsSync(binPath)) {
|
|
163
|
+
console.error(`[cli/publish] bundle did not produce ${binPath}; aborting`);
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Mark the bin entry executable BEFORE packing. `tsc` emits 644; npm preserves
|
|
168
|
+
// file modes in the tarball, and pnpm doesn't always chmod bin entries to 755
|
|
169
|
+
// when installing from a tarball URL (only registry installs and named npm
|
|
170
|
+
// packages get this treatment reliably). Without this step, `fimo` fails with
|
|
171
|
+
// `zsh: permission denied` after `npm i -g <tarball-url>`.
|
|
172
|
+
chmodSync(binPath, 0o755);
|
|
173
|
+
|
|
174
|
+
// 4. Pack into a tarball, forced to the CLI package root. Remove any
|
|
175
|
+
// previous `.tgz` first so `pnpm pack` never bails out on "file exists".
|
|
176
|
+
rmSync(tarballPath, { force: true });
|
|
177
|
+
run('pnpm', ['pack', '--pack-destination', CLI_ROOT], { cwd: CLI_ROOT });
|
|
178
|
+
if (!existsSync(tarballPath)) {
|
|
179
|
+
console.error(`[cli/publish] expected tarball not found at ${tarballPath}`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const releaseKey = `cli/${pkg.name}-${releaseId}.tgz`;
|
|
184
|
+
const latestKey = `cli/${pkg.name}-latest.tgz`;
|
|
185
|
+
|
|
186
|
+
console.log(`[cli/publish] tarball: ${tarballPath} (${(statSync(tarballPath).size / 1024).toFixed(1)} KB)`);
|
|
187
|
+
console.log(`[cli/publish] uploading to r2://${bucket}/${releaseKey}`);
|
|
188
|
+
|
|
189
|
+
// 5. Upload via wrangler. Always upload the release-keyed object. Skip the
|
|
190
|
+
// `latest` alias when a custom `--id` is in play - dev channels shouldn't
|
|
191
|
+
// overwrite the public install URL that real users pull from.
|
|
192
|
+
//
|
|
193
|
+
// Both the release-keyed URL and the `latest` alias are tagged with
|
|
194
|
+
// `no-cache` so republishing propagates immediately. For random-hex
|
|
195
|
+
// release ids (no `--id`), nothing else ever lives at that key, so the
|
|
196
|
+
// only practical effect is freshness on the first install. For stable
|
|
197
|
+
// `--id <name>` dev channels (e.g. `marc-dev`), no-cache is essential:
|
|
198
|
+
// the URL is reused across publishes, and without it Cloudflare's edge
|
|
199
|
+
// cache + pnpm's URL-keyed store will hand out stale tarballs after
|
|
200
|
+
// a republish (Node sees fimo's `virtual:` imports because the old
|
|
201
|
+
// code didn't bundle them into SSR, etc).
|
|
202
|
+
const wranglerArgs = ['exec', 'wrangler', 'r2', 'object', 'put'];
|
|
203
|
+
if (remote) {
|
|
204
|
+
wranglerArgs.push('--remote');
|
|
205
|
+
}
|
|
206
|
+
run(
|
|
207
|
+
'pnpm',
|
|
208
|
+
[...wranglerArgs, `${bucket}/${releaseKey}`, '--file', tarballPath, '--cache-control', 'no-cache, max-age=0'],
|
|
209
|
+
{ cwd: CLI_ROOT }
|
|
210
|
+
);
|
|
211
|
+
if (!customId) {
|
|
212
|
+
run(
|
|
213
|
+
'pnpm',
|
|
214
|
+
[...wranglerArgs, `${bucket}/${latestKey}`, '--file', tarballPath, '--cache-control', 'no-cache, max-age=0'],
|
|
215
|
+
{ cwd: CLI_ROOT }
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const PUBLIC_BASE_URL = 'https://pub-41cdea46386f4b238d8c528c4327dfc1.r2.dev';
|
|
220
|
+
|
|
221
|
+
console.log('[cli/publish] done.');
|
|
222
|
+
console.log(` release: ${PUBLIC_BASE_URL}/${releaseKey}`);
|
|
223
|
+
if (!customId) {
|
|
224
|
+
console.log(` latest: ${PUBLIC_BASE_URL}/${latestKey}`);
|
|
225
|
+
} else {
|
|
226
|
+
console.log(` latest: (skipped - custom --id ${customId} set, leaving fimo-latest.tgz untouched)`);
|
|
227
|
+
}
|
|
228
|
+
console.log('');
|
|
229
|
+
console.log('Install on a test machine:');
|
|
230
|
+
console.log(` npm install -g ${PUBLIC_BASE_URL}/${latestKey}`);
|
|
231
|
+
|
|
232
|
+
// Explicit cleanup at the end of the happy path. The process.on('exit')
|
|
233
|
+
// handler also covers this, but an explicit call lets us print a
|
|
234
|
+
// confirmation line so users can see release.json went away.
|
|
235
|
+
cleanupReleaseJson();
|
|
236
|
+
console.log('');
|
|
237
|
+
if (shouldKeep) {
|
|
238
|
+
console.log(
|
|
239
|
+
`[cli/publish] kept workspace release.json (id: ${releaseId}) - new \`fimo create\` scaffolds will pin to this URL.`
|
|
240
|
+
);
|
|
241
|
+
} else {
|
|
242
|
+
console.log(
|
|
243
|
+
`[cli/publish] cleaned up workspace release.json - local \`fimo\` invocations will resolve to localhost:3000 again.`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "react-router dev --port 5173",
|
|
8
|
+
"prebuild": "fimo validate",
|
|
8
9
|
"build": "react-router build && fimo export-static",
|
|
9
10
|
"lint": "oxlint --deny-warnings",
|
|
10
11
|
"typecheck": "tsc --noEmit",
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
"cmdk": "^1.1.1",
|
|
22
23
|
"date-fns": "^4.1.0",
|
|
23
24
|
"embla-carousel-react": "^8.6.0",
|
|
24
|
-
"fimo": "0.2.3-staging.
|
|
25
|
+
"fimo": "0.2.3-staging.7",
|
|
25
26
|
"input-otp": "^1.4.2",
|
|
26
27
|
"isbot": "^5",
|
|
27
28
|
"lucide-react": "^0.577.0",
|
|
@@ -52,5 +53,11 @@
|
|
|
52
53
|
"tw-animate-css": "^1.4.0",
|
|
53
54
|
"typescript": "5.9.3",
|
|
54
55
|
"vite": "^8.0.13"
|
|
56
|
+
},
|
|
57
|
+
"pnpm": {
|
|
58
|
+
"onlyBuiltDependencies": [
|
|
59
|
+
"esbuild",
|
|
60
|
+
"fimo"
|
|
61
|
+
]
|
|
55
62
|
}
|
|
56
63
|
}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
# here: pnpm refuses to have both an allowlist and allow-all-builds
|
|
5
|
-
# (ERR_PNPM_CONFIG_CONFLICT_BUILT_DEPENDENCIES), which makes sandbox installs
|
|
6
|
-
# fail before the project's `fimo` dep is available.
|
|
1
|
+
allowBuilds:
|
|
2
|
+
esbuild: true
|
|
3
|
+
fimo: true
|