diff --git a/TODO.md b/TODO.md
new file mode 100644
index 00000000..52d6133a
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,57 @@
+# v3 TODO List
+
+Tracking items identified during the v2 → v3 migration audit.
+
+---
+
+## Commands
+
+- [ ] Implement `craft` command (drives full build from `craft.yml`; should be easier with v3 vendor/registry mode)
+- [x] Migrate `micro:combine` command (combine `micro.sfx` with PHP code + INI injection)
+- [ ] Implement `dump-extensions` command (extract required extensions from `composer.json` / `composer.lock`)
+- [ ] Design and implement v3 dev toolchain commands (WIP — needs design decision):
+ - [ ] `dev:extensions` / equivalent listing command
+ - [ ] `dev:php-version`, `dev:ext-version`, `dev:lib-version`
+ - [ ] Doc generation commands (`dev:gen-ext-docs`, `dev:gen-ext-dep-docs`, `dev:gen-lib-dep-docs`) — pending v3 doc design
+
+---
+
+## Source Patches (SourcePatcher → Artifact migration)
+
+The following v2 `SourcePatcher` hooks are not yet migrated to v3 `src/Package/Artifact/` classes:
+
+- [ ] Migrate `patchSQLSRVWin32` — removes `/sdl` compile flag to prevent Zend build failure on Windows
+- [ ] Migrate `patchSQLSRVPhp85` — fixes `pdo_sqlsrv` directory layout for PHP 8.5
+- [ ] Migrate `patchYamlWin32` — patches `config.w32` `_a.lib` detection logic for the `yaml` extension
+- [ ] Migrate `patchImagickWith84` — applies PHP 8.4 compatibility patch for `imagick` based on version detection
+
+---
+
+## Extension Package Classes (Unix)
+
+Extensions that had non-trivial v2 build logic and are missing a v3 `src/Package/Extension/` class:
+
+- [x] `gettext` — macOS: fix `config.m4` bracket syntax for cross-version compatibility + append frameworks to linker flags (critical for macOS linking; this is a Unix-side gap, not Windows-only)
+
+---
+
+## Windows Extensions (Early Stage)
+
+Windows extension support is still in early stage. The following extensions had Windows-specific configure args or patches in v2 and are pending v3 Windows implementation:
+
+- [ ] `amqp` — Windows configure args
+- [ ] `com_dotnet` — Windows-only extension
+- [ ] `dom` — remove `dllmain.c` from `config.w32`
+- [ ] `ev` — fix `PHP_EV_SHARED` in `config.w32`
+- [ ] `gmssl` — add `CHECK_LIB("gmssl.lib")` to `config.w32`
+- [ ] `intl` — fix `PHP_INTL_SHARED` in `config.w32`
+- [ ] `lz4` — Windows configure args
+- [ ] `mbregex` — Windows configure args
+- [ ] `sqlsrv` / `pdo_sqlsrv` — complex conditional build logic (independent `sqlsrv` without `pdo_sqlsrv`)
+- [ ] `xml` — remove `dllmain.c` from `config.w32`; handles `soap`, `xmlreader`, `xmlwriter`, `simplexml`
+
+---
+
+## Documentation
+
+- [ ] Write v3 user documentation (currently zero v3 docs)
diff --git a/src/StaticPHP/Command/MicroCombineCommand.php b/src/StaticPHP/Command/MicroCombineCommand.php
new file mode 100644
index 00000000..b46d995f
--- /dev/null
+++ b/src/StaticPHP/Command/MicroCombineCommand.php
@@ -0,0 +1,120 @@
+addArgument('file', InputArgument::REQUIRED, 'The php or phar file to be combined');
+ $this->addOption('with-micro', 'M', InputOption::VALUE_REQUIRED, 'Customize your micro.sfx file');
+ $this->addOption('with-ini-set', 'I', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'ini to inject into micro.sfx when combining');
+ $this->addOption('with-ini-file', 'N', InputOption::VALUE_REQUIRED, 'ini file to inject into micro.sfx when combining');
+ $this->addOption('output', 'O', InputOption::VALUE_REQUIRED, 'Customize your output binary file name');
+ }
+
+ public function handle(): int
+ {
+ // 0. Initialize path variables
+ $internal = FileSystem::convertPath(BUILD_ROOT_PATH . '/bin/micro.sfx');
+ $micro_file = $this->getOption('with-micro');
+ $file = $this->getArgument('file');
+ $ini_set = $this->getOption('with-ini-set');
+ $ini_file = $this->getOption('with-ini-file');
+ $target_ini = [];
+ $output = $this->getOption('output') ?? 'my-app';
+ $ini_part = '';
+ // 1. Make sure specified micro.sfx file exists
+ if ($micro_file !== null && !file_exists($micro_file)) {
+ $this->output->writeln('The micro.sfx file you specified is incorrect or does not exist!');
+ return static::FAILURE;
+ }
+ // 2. Make sure buildroot/bin/micro.sfx exists
+ if ($micro_file === null && !file_exists($internal)) {
+ $this->output->writeln('You haven\'t compiled micro.sfx yet, please use "build" command and "--build-micro" to compile phpmicro first!');
+ return static::FAILURE;
+ }
+ // 3. Use buildroot/bin/micro.sfx
+ if ($micro_file === null) {
+ $micro_file = $internal;
+ }
+ // 4. Make sure php or phar file exists
+ if (!is_file(FileSystem::convertPath($file))) {
+ $this->output->writeln('The file to combine does not exist!');
+ return static::FAILURE;
+ }
+ // 5. Confirm ini files (ini-set has higher priority)
+ if ($ini_file !== null) {
+ // Check file exist first
+ if (!file_exists($ini_file)) {
+ $this->output->writeln('The ini file to combine does not exist! (' . $ini_file . ')');
+ return static::FAILURE;
+ }
+ $arr = parse_ini_file($ini_file);
+ if ($arr === false) {
+ $this->output->writeln('Cannot parse ini file');
+ return static::FAILURE;
+ }
+ $target_ini = array_merge($target_ini, $arr);
+ }
+ // 6. Confirm ini sets
+ if ($ini_set !== []) {
+ foreach ($ini_set as $item) {
+ $arr = parse_ini_string($item);
+ if ($arr === false) {
+ $this->output->writeln('--with-ini-set parse failed');
+ return static::FAILURE;
+ }
+ $target_ini = array_merge($target_ini, $arr);
+ }
+ }
+ // 7. Generate ini injection parts
+ if (!empty($target_ini)) {
+ $ini_str = $this->encodeINI($target_ini);
+ logger()->debug('Injecting ini parts: ' . PHP_EOL . $ini_str);
+ $ini_part = "\xfd\xf6\x69\xe6";
+ $ini_part .= pack('N', strlen($ini_str));
+ $ini_part .= $ini_str;
+ }
+ // 8. Combine !
+ $output = FileSystem::isRelativePath($output) ? (WORKING_DIR . '/' . $output) : $output;
+ $file_target = file_get_contents($micro_file) . $ini_part . file_get_contents($file);
+ if (PHP_OS_FAMILY === 'Windows' && !str_ends_with(strtolower($output), '.exe')) {
+ $output .= '.exe';
+ }
+ $output = FileSystem::convertPath($output);
+ $result = file_put_contents($output, $file_target);
+ if ($result === false) {
+ $this->output->writeln('Combine failed.');
+ return static::FAILURE;
+ }
+ // 9. chmod +x
+ chmod($output, 0755);
+ $this->output->writeln('Combine success! Binary file: ' . $output . '');
+ return static::SUCCESS;
+ }
+
+ private function encodeINI(array $array): string
+ {
+ $res = [];
+ foreach ($array as $key => $val) {
+ if (is_array($val)) {
+ $res[] = "[{$key}]";
+ foreach ($val as $skey => $sval) {
+ $res[] = "{$skey}=" . (is_numeric($sval) ? $sval : '"' . $sval . '"');
+ }
+ } else {
+ $res[] = "{$key}=" . (is_numeric($val) ? $val : '"' . $val . '"');
+ }
+ }
+ return implode("\n", $res);
+ }
+}
diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php
index 2882b574..0c8fec83 100644
--- a/src/StaticPHP/ConsoleApplication.php
+++ b/src/StaticPHP/ConsoleApplication.php
@@ -20,6 +20,7 @@ use StaticPHP\Command\DownloadCommand;
use StaticPHP\Command\DumpLicenseCommand;
use StaticPHP\Command\ExtractCommand;
use StaticPHP\Command\InstallPackageCommand;
+use StaticPHP\Command\MicroCombineCommand;
use StaticPHP\Command\ResetCommand;
use StaticPHP\Command\SPCConfigCommand;
use StaticPHP\Package\TargetPackage;
@@ -65,6 +66,7 @@ class ConsoleApplication extends Application
new DumpLicenseCommand(),
new ResetCommand(),
new CheckUpdateCommand(),
+ new MicroCombineCommand(),
// dev commands
new ShellCommand(),