Skip to content

Commit c6b2da9

Browse files
committed
feat: create 'flux init' command to support, flutter, react-native and expo applications
1 parent 77eb51f commit c6b2da9

55 files changed

Lines changed: 3862 additions & 18 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

bin/flux.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/usr/bin/env node
2+
import { Command } from 'commander';
3+
import chalk from 'chalk';
4+
import { execa } from 'execa';
5+
import ora from 'ora';
6+
import fs from 'fs';
7+
import YAML from 'yaml';
8+
9+
const program = new Command();
10+
11+
program
12+
.name('flux')
13+
.description('Local CI/CD CLI for mobile apps 📱🚀')
14+
.version('0.0.1');
15+
16+
program
17+
.command('init')
18+
.description('Create flux.yml and (for Flutter) register it in pubspec.yaml')
19+
.option('-f, --force', 'overwrite existing flux.yml (backups will be created)', false)
20+
.action(async (opts) => {
21+
try {
22+
await initCommand(process.cwd(), { force: !!opts.force });
23+
process.exit(0);
24+
} catch (err) {
25+
process.exit(1);
26+
}
27+
});
28+
29+
program.parse();

index.js

Lines changed: 0 additions & 14 deletions
This file was deleted.

lib/init.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// lib/init.js
2+
import fs from 'fs-extra';
3+
import path from 'path';
4+
import os from 'os';
5+
import YAML from 'yaml';
6+
import chalk from 'chalk';
7+
8+
const DEFAULT_FLUX_YML = (framework) => ({
9+
app_name: 'My App',
10+
platform: framework,
11+
android: {
12+
keystore_path: './android/keystore.jks',
13+
key_alias: 'mykey',
14+
key_password: '',
15+
store_password: ''
16+
},
17+
ios: {
18+
enabled: framework !== 'react-native' ? true : false,
19+
export_method: 'app-store'
20+
},
21+
playstore: {
22+
service_account_json: './keys/playstore.json'
23+
},
24+
versioning: {
25+
strategy: 'auto' // options: auto, manual
26+
}
27+
});
28+
29+
function timestamp() {
30+
const d = new Date();
31+
return d.toISOString().replace(/[:.]/g, '-');
32+
}
33+
34+
async function writeYamlFile(filePath, data) {
35+
const yamlStr = YAML.stringify(data);
36+
await fs.outputFile(filePath, yamlStr, { encoding: 'utf8' });
37+
}
38+
39+
async function detectProjectType(projectDir) {
40+
const pubspecPath = path.join(projectDir, 'pubspec.yaml');
41+
const packageJsonPath = path.join(projectDir, 'package.json');
42+
const androidDir = path.join(projectDir, 'android');
43+
const iosDir = path.join(projectDir, 'ios');
44+
45+
const hasPubspec = await fs.pathExists(pubspecPath);
46+
const hasPackage = await fs.pathExists(packageJsonPath);
47+
const hasAndroid = await fs.pathExists(androidDir);
48+
const hasIos = await fs.pathExists(iosDir);
49+
50+
if (hasPubspec) {
51+
const pubspecContent = await fs.readFile(pubspecPath, 'utf8');
52+
if (pubspecContent.includes('flutter:')) return 'flutter';
53+
}
54+
55+
if (hasPackage) {
56+
try {
57+
const pkg = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
58+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
59+
60+
if (deps['expo']) return 'expo';
61+
if (deps['react-native']) return 'react-native';
62+
if (deps['@react-navigation/native']) return 'react-native';
63+
} catch (err) {
64+
console.warn(chalk.yellow('Warning: could not parse package.json:'), err.message);
65+
}
66+
}
67+
68+
// fallback heuristic
69+
if (hasAndroid && hasIos) return 'react-native';
70+
71+
return 'unknown';
72+
}
73+
74+
/**
75+
* Ensure flux.yml exists; if exist and !force -> throw
76+
*/
77+
async function createFluxYml(projectDir, framework, opts = { force: false }) {
78+
const fluxPath = path.join(projectDir, 'flux.yml');
79+
const exists = await fs.pathExists(fluxPath);
80+
if (exists && !opts.force) {
81+
throw new Error('flux.yml already exists. Use --force to overwrite.');
82+
}
83+
84+
if (exists && opts.force) {
85+
const backup = path.join(projectDir, `flux.yml.bak.${timestamp()}`);
86+
await fs.copyFile(fluxPath, backup);
87+
}
88+
89+
await writeYamlFile(fluxPath, DEFAULT_FLUX_YML(framework));
90+
return fluxPath;
91+
}
92+
93+
/**
94+
* Safely update pubspec.yaml to include flux.yml in flutter.assets.
95+
*/
96+
async function registerAssetInPubspec(projectDir, assetRelativePath) {
97+
const pubspecPath = path.join(projectDir, 'pubspec.yaml');
98+
const exists = await fs.pathExists(pubspecPath);
99+
if (!exists) throw new Error('pubspec.yaml not found — this does not look like a Flutter project.');
100+
101+
const original = await fs.readFile(pubspecPath, 'utf8');
102+
const doc = YAML.parseDocument(original);
103+
104+
const backupPath = path.join(projectDir, `pubspec.yaml.bak.${timestamp()}`);
105+
await fs.copyFile(pubspecPath, backupPath);
106+
107+
let flutterNode = doc.get('flutter', true);
108+
if (!flutterNode) {
109+
doc.set('flutter', {});
110+
flutterNode = doc.get('flutter', true);
111+
}
112+
113+
let assetsNode = flutterNode.get('assets', true);
114+
if (!assetsNode) {
115+
doc.get('flutter').set('assets', []);
116+
assetsNode = doc.getIn(['flutter', 'assets'], true);
117+
}
118+
119+
const assets = doc.getIn(['flutter', 'assets']).toJSON ? doc.getIn(['flutter', 'assets']).toJSON() : doc.getIn(['flutter', 'assets']);
120+
121+
let normalized = assetRelativePath.replace(/\\/g, '/');
122+
if (normalized.startsWith('./')) normalized = normalized.slice(2);
123+
124+
if (assets.includes(normalized)) {
125+
return { updated: false, backupPath };
126+
}
127+
128+
const seq = doc.getIn(['flutter', 'assets']);
129+
seq.add(normalized);
130+
131+
const newYaml = doc.toString();
132+
await fs.writeFile(pubspecPath, newYaml, 'utf8');
133+
134+
return { updated: true, backupPath };
135+
}
136+
137+
/**
138+
* Public entry point for init
139+
*/
140+
export async function initCommand(projectDir = process.cwd(), opts = { force: false }) {
141+
try {
142+
const framework = await detectProjectType(projectDir);
143+
console.log(chalk.cyan(`Detected project type: ${framework}`));
144+
145+
const createdFlux = await createFluxYml(projectDir, framework, { force: opts.force });
146+
console.log(chalk.green(`Created ${path.relative(process.cwd(), createdFlux)}`));
147+
148+
if (framework === 'flutter') {
149+
const { updated, backupPath } = await registerAssetInPubspec(projectDir, './flux.yml');
150+
if (updated) {
151+
console.log(chalk.green(`Registered flux.yml in pubspec.yaml (backup saved to ${path.basename(backupPath)})`));
152+
console.log(chalk.yellow('Note: run `flutter pub get` if you plan to load the asset at runtime.'));
153+
} else {
154+
console.log(chalk.gray('flux.yml already registered in pubspec.yaml — no change.'));
155+
}
156+
} else if (framework === 'react-native' || framework === 'expo') {
157+
console.log(chalk.yellow(`${framework} project detected — no automatic asset registration needed.`));
158+
console.log(chalk.yellow('If you want to bundle flux.yml, configure Metro or copy file into your app assets.'));
159+
} else {
160+
console.log(chalk.yellow('Unknown project type — flux.yml created, but no further project changes made.'));
161+
}
162+
163+
console.log(chalk.green('Initialization complete 🎉'));
164+
} catch (err) {
165+
console.error(chalk.red('Initialization failed:'), err.message);
166+
throw err;
167+
}
168+
}

node_modules/.package-lock.json

Lines changed: 42 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

node_modules/fs-extra/LICENSE

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)