diff --git a/.github/workflows/resource-request.yml b/.github/workflows/resource-request.yml index 7005a40a..5bc56933 100644 --- a/.github/workflows/resource-request.yml +++ b/.github/workflows/resource-request.yml @@ -240,7 +240,7 @@ jobs: uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - claude_args: "--allowedTools Bash,Read,Edit,Write --max-turns 30" + claude_args: "--allowedTools Bash,Read,Edit,Write --max-turns 120" prompt: | The integration tests for the newly generated resource failed on Linux (ubuntu-latest). @@ -343,7 +343,7 @@ jobs: uses: anthropics/claude-code-action@v1 with: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} - claude_args: "--allowedTools Bash,Read,Edit,Write --max-turns 30" + claude_args: "--allowedTools Bash,Read,Edit,Write --max-turns 120" prompt: | The integration tests for the newly generated resource failed on macOS (macos-latest). diff --git a/CLAUDE.md b/CLAUDE.md index d4494a7f..6e0c3d22 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -369,11 +369,24 @@ The Codify Editor supports auto-complete for certain resource parameters (e.g. H ### Adding completions for a parameter -1. Create `src/resources///completions/..ts` +1. Create `src/resources///completions/..ts` 2. Export a default async function returning `Promise` — fetch the values, return them, nothing else -3. The filename determines the Supabase metadata automatically: - - `homebrew.formulae.ts` → `resource_type=homebrew`, `parameter_path=/formulae` -4. Run `npm run build:completions` to regenerate the index +3. The filename encodes both the resource type and the JSONPath of the parameter: + - Everything **before the first dot** = `resource_type` (e.g. `homebrew`) + - Everything **after the first dot** = JSONPath expression (e.g. `$.formulae`) + - Examples: + - `homebrew.$.formulae.ts` → `resource_type=homebrew`, `parameter_path=$.formulae` + - `nvm.$.nodeVersions.ts` → `resource_type=nvm`, `parameter_path=$.nodeVersions` + - `codex.$.config.model.ts` → `resource_type=codex`, `parameter_path=$.config.model` +4. For parameters **nested inside array items** (e.g. a property on each object in an array), use `[x]` in the filename to encode the `[*]` array wildcard — bundlers treat `[*]` as a glob pattern in import paths, so `[x]` is used as the safe filename equivalent and is translated to `[*]` by the codegen script: + - `ios-simulators.$.simulators[x].deviceType.ts` → `parameter_path=$.simulators[*].deviceType` +5. For **mirror completions** — where a parameter's suggestions should reflect the current value of a sibling parameter on the same resource — export a plain object instead of a fetch function: + ```typescript + // xcodes.$.selected.ts — offers whatever the user typed in xcodeVersions + export default { mirrorParameter: '$.xcodeVersions' } as const; + ``` + The codegen script detects the export type at build time. The cron job writes a single metadata row to Supabase (with `mirror_parameter_path` set and no `value` rows). The dashboard reads this and serves completions client-side from the resource's current config — no DB query needed. +6. Run `npm run build:completions` to regenerate the index ```bash npm run build:completions # regenerate completions-cron/src/__generated__/completions-index.ts diff --git a/completions-cron/src/__generated__/completions-index.ts b/completions-cron/src/__generated__/completions-index.ts index e79ceb68..5f853e2c 100644 --- a/completions-cron/src/__generated__/completions-index.ts +++ b/completions-cron/src/__generated__/completions-index.ts @@ -1,74 +1,170 @@ // AUTO-GENERATED by scripts/generate-completions-index.ts - DO NOT EDIT // Re-run `npm run build:completions` to regenerate -import mod0 from '../../../src/resources/xcodes/completions/xcodes.xcodeVersions.js'; -import mod1 from '../../../src/resources/webstorm/completions/webstorm.plugins.js'; -import mod2 from '../../../src/resources/vscode/completions/vscode.extensions.js'; -import mod3 from '../../../src/resources/snap/completions/snap.install.js'; -import mod4 from '../../../src/resources/ruby/rbenv/completions/rbenv.rubyVersions.js'; -import mod5 from '../../../src/resources/python/uv/completions/uv.tools.js'; -import mod6 from '../../../src/resources/python/uv/completions/uv.pythonVersions.js'; -import mod7 from '../../../src/resources/python/pyenv/completions/pyenv.pythonVersions.js'; -import mod8 from '../../../src/resources/python/pip/completions/pip.install.js'; -import mod9 from '../../../src/resources/ollama/completions/ollama.models.js'; -import mod10 from '../../../src/resources/jetbrains/rustrover/completions/rustrover.plugins.js'; -import mod11 from '../../../src/resources/jetbrains/rubymine/completions/rubymine.plugins.js'; -import mod12 from '../../../src/resources/jetbrains/rider/completions/rider.plugins.js'; -import mod13 from '../../../src/resources/jetbrains/pycharm/completions/pycharm.plugins.js'; -import mod14 from '../../../src/resources/jetbrains/phpstorm/completions/phpstorm.plugins.js'; -import mod15 from '../../../src/resources/jetbrains/intellij-idea/completions/intellij-idea.plugins.js'; -import mod16 from '../../../src/resources/jetbrains/goland/completions/goland.plugins.js'; -import mod17 from '../../../src/resources/jetbrains/clion/completions/clion.plugins.js'; -import mod18 from '../../../src/resources/javascript/pnpm/completions/pnpm.globalEnvNodeVersion.js'; -import mod19 from '../../../src/resources/javascript/nvm/completions/nvm.nodeVersions.js'; -import mod20 from '../../../src/resources/javascript/npm/completions/npm.install.js'; -import mod21 from '../../../src/resources/homebrew/completions/homebrew.formulae.js'; -import mod22 from '../../../src/resources/homebrew/completions/homebrew.casks.js'; -import mod23 from '../../../src/resources/cursor/completions/cursor.extensions.js'; -import mod24 from '../../../src/resources/codex/completions/codex.config.model.js'; -import mod25 from '../../../src/resources/asdf/completions/asdf.plugins.js'; -import mod26 from '../../../src/resources/asdf/completions/asdf-plugin.plugin.js'; -import mod27 from '../../../src/resources/apt/completions/apt.install.js'; -import mod28 from '../../../src/resources/android/android-studios/completions/android-studio.version.js'; -import mod29 from '../../../src/resources/android/android-cli/completions/android-cli.sdkPackages.js'; -import mod30 from '../../../src/resources/android/android-cli/completions/android-cli.emulators.js'; +import mod0 from '../../../src/resources/xcodes/completions/xcodes.$.xcodeVersions.js'; +import mod1 from '../../../src/resources/xcodes/completions/xcodes.$.selected.js'; +import mod2 from '../../../src/resources/webstorm/completions/webstorm.$.plugins.js'; +import mod3 from '../../../src/resources/vscode/completions/vscode.$.extensions.js'; +import mod4 from '../../../src/resources/snap/completions/snap.$.install.js'; +import mod5 from '../../../src/resources/ruby/rbenv/completions/rbenv.$.rubyVersions.js'; +import mod6 from '../../../src/resources/ruby/rbenv/completions/rbenv.$.global.js'; +import mod7 from '../../../src/resources/python/uv/completions/uv.$.tools.js'; +import mod8 from '../../../src/resources/python/uv/completions/uv.$.pythonVersions.js'; +import mod9 from '../../../src/resources/python/pyenv/completions/pyenv.$.pythonVersions.js'; +import mod10 from '../../../src/resources/python/pyenv/completions/pyenv.$.global.js'; +import mod11 from '../../../src/resources/python/pip/completions/pip.$.install.js'; +import mod12 from '../../../src/resources/ollama/completions/ollama.$.models.js'; +import mod13 from '../../../src/resources/jetbrains/rustrover/completions/rustrover.$.plugins.js'; +import mod14 from '../../../src/resources/jetbrains/rubymine/completions/rubymine.$.plugins.js'; +import mod15 from '../../../src/resources/jetbrains/rider/completions/rider.$.plugins.js'; +import mod16 from '../../../src/resources/jetbrains/pycharm/completions/pycharm.$.plugins.js'; +import mod17 from '../../../src/resources/jetbrains/phpstorm/completions/phpstorm.$.plugins.js'; +import mod18 from '../../../src/resources/jetbrains/intellij-idea/completions/intellij-idea.$.plugins.js'; +import mod19 from '../../../src/resources/jetbrains/goland/completions/goland.$.plugins.js'; +import mod20 from '../../../src/resources/jetbrains/clion/completions/clion.$.plugins.js'; +import mod21 from '../../../src/resources/javascript/pnpm/completions/pnpm.$.globalEnvNodeVersion.js'; +import mod22 from '../../../src/resources/javascript/nvm/completions/nvm.$.nodeVersions.js'; +import mod23 from '../../../src/resources/javascript/nvm/completions/nvm.$.global.js'; +import mod24 from '../../../src/resources/javascript/npm/completions/npm.$.install.js'; +import mod25 from '../../../src/resources/javascript/fast-node-manager/completions/fast-node-manager.$.defaultVersion.js'; +import mod26 from '../../../src/resources/java/jenv/completions/jenv.$.global.js'; +import mod27 from '../../../src/resources/ios/ios-simulator/completions/ios-simulators.$.simulators[x].runtime.js'; +import mod28 from '../../../src/resources/ios/ios-simulator/completions/ios-simulators.$.simulators[x].deviceType.js'; +import mod29 from '../../../src/resources/homebrew/completions/homebrew.$.formulae.js'; +import mod30 from '../../../src/resources/homebrew/completions/homebrew.$.casks.js'; +import mod31 from '../../../src/resources/go/goenv/completions/goenv.$.global.js'; +import mod32 from '../../../src/resources/cursor/completions/cursor.$.extensions.js'; +import mod33 from '../../../src/resources/codex/completions/codex.$.config.model.js'; +import mod34 from '../../../src/resources/asdf/completions/asdf.$.plugins.js'; +import mod35 from '../../../src/resources/asdf/completions/asdf-plugin.$.plugin.js'; +import mod36 from '../../../src/resources/apt/completions/apt.$.install.js'; +import mod37 from '../../../src/resources/android/android-studios/completions/android-studio.$.version.js'; +import mod38 from '../../../src/resources/android/android-cli/completions/android-cli.$.sdkPackages.js'; +import mod39 from '../../../src/resources/android/android-cli/completions/android-cli.$.emulators.js'; -export interface CompletionModule { - resourceType: string - parameterPath: string - fetch: () => Promise -} +export type CompletionModule = + | { kind: 'fetch'; resourceType: string; parameterPath: string; fetch: () => Promise } + | { kind: 'mirror'; resourceType: string; parameterPath: string; mirrorParameter: string } export const completionModules: CompletionModule[] = [ - { resourceType: 'xcodes', parameterPath: '/xcodeVersions', fetch: mod0 }, - { resourceType: 'webstorm', parameterPath: '/plugins', fetch: mod1 }, - { resourceType: 'vscode', parameterPath: '/extensions', fetch: mod2 }, - { resourceType: 'snap', parameterPath: '/install', fetch: mod3 }, - { resourceType: 'rbenv', parameterPath: '/rubyVersions', fetch: mod4 }, - { resourceType: 'uv', parameterPath: '/tools', fetch: mod5 }, - { resourceType: 'uv', parameterPath: '/pythonVersions', fetch: mod6 }, - { resourceType: 'pyenv', parameterPath: '/pythonVersions', fetch: mod7 }, - { resourceType: 'pip', parameterPath: '/install', fetch: mod8 }, - { resourceType: 'ollama', parameterPath: '/models', fetch: mod9 }, - { resourceType: 'rustrover', parameterPath: '/plugins', fetch: mod10 }, - { resourceType: 'rubymine', parameterPath: '/plugins', fetch: mod11 }, - { resourceType: 'rider', parameterPath: '/plugins', fetch: mod12 }, - { resourceType: 'pycharm', parameterPath: '/plugins', fetch: mod13 }, - { resourceType: 'phpstorm', parameterPath: '/plugins', fetch: mod14 }, - { resourceType: 'intellij-idea', parameterPath: '/plugins', fetch: mod15 }, - { resourceType: 'goland', parameterPath: '/plugins', fetch: mod16 }, - { resourceType: 'clion', parameterPath: '/plugins', fetch: mod17 }, - { resourceType: 'pnpm', parameterPath: '/globalEnvNodeVersion', fetch: mod18 }, - { resourceType: 'nvm', parameterPath: '/nodeVersions', fetch: mod19 }, - { resourceType: 'npm', parameterPath: '/install', fetch: mod20 }, - { resourceType: 'homebrew', parameterPath: '/formulae', fetch: mod21 }, - { resourceType: 'homebrew', parameterPath: '/casks', fetch: mod22 }, - { resourceType: 'cursor', parameterPath: '/extensions', fetch: mod23 }, - { resourceType: 'codex', parameterPath: '/config/model', fetch: mod24 }, - { resourceType: 'asdf', parameterPath: '/plugins', fetch: mod25 }, - { resourceType: 'asdf-plugin', parameterPath: '/plugin', fetch: mod26 }, - { resourceType: 'apt', parameterPath: '/install', fetch: mod27 }, - { resourceType: 'android-studio', parameterPath: '/version', fetch: mod28 }, - { resourceType: 'android-cli', parameterPath: '/sdkPackages', fetch: mod29 }, - { resourceType: 'android-cli', parameterPath: '/emulators', fetch: mod30 }, + typeof mod0 === 'function' + ? { kind: 'fetch', resourceType: 'xcodes', parameterPath: '$.xcodeVersions', fetch: mod0 } + : { kind: 'mirror', resourceType: 'xcodes', parameterPath: '$.xcodeVersions', mirrorParameter: (mod0 as any).mirrorParameter }, + typeof mod1 === 'function' + ? { kind: 'fetch', resourceType: 'xcodes', parameterPath: '$.selected', fetch: mod1 } + : { kind: 'mirror', resourceType: 'xcodes', parameterPath: '$.selected', mirrorParameter: (mod1 as any).mirrorParameter }, + typeof mod2 === 'function' + ? { kind: 'fetch', resourceType: 'webstorm', parameterPath: '$.plugins', fetch: mod2 } + : { kind: 'mirror', resourceType: 'webstorm', parameterPath: '$.plugins', mirrorParameter: (mod2 as any).mirrorParameter }, + typeof mod3 === 'function' + ? { kind: 'fetch', resourceType: 'vscode', parameterPath: '$.extensions', fetch: mod3 } + : { kind: 'mirror', resourceType: 'vscode', parameterPath: '$.extensions', mirrorParameter: (mod3 as any).mirrorParameter }, + typeof mod4 === 'function' + ? { kind: 'fetch', resourceType: 'snap', parameterPath: '$.install', fetch: mod4 } + : { kind: 'mirror', resourceType: 'snap', parameterPath: '$.install', mirrorParameter: (mod4 as any).mirrorParameter }, + typeof mod5 === 'function' + ? { kind: 'fetch', resourceType: 'rbenv', parameterPath: '$.rubyVersions', fetch: mod5 } + : { kind: 'mirror', resourceType: 'rbenv', parameterPath: '$.rubyVersions', mirrorParameter: (mod5 as any).mirrorParameter }, + typeof mod6 === 'function' + ? { kind: 'fetch', resourceType: 'rbenv', parameterPath: '$.global', fetch: mod6 } + : { kind: 'mirror', resourceType: 'rbenv', parameterPath: '$.global', mirrorParameter: (mod6 as any).mirrorParameter }, + typeof mod7 === 'function' + ? { kind: 'fetch', resourceType: 'uv', parameterPath: '$.tools', fetch: mod7 } + : { kind: 'mirror', resourceType: 'uv', parameterPath: '$.tools', mirrorParameter: (mod7 as any).mirrorParameter }, + typeof mod8 === 'function' + ? { kind: 'fetch', resourceType: 'uv', parameterPath: '$.pythonVersions', fetch: mod8 } + : { kind: 'mirror', resourceType: 'uv', parameterPath: '$.pythonVersions', mirrorParameter: (mod8 as any).mirrorParameter }, + typeof mod9 === 'function' + ? { kind: 'fetch', resourceType: 'pyenv', parameterPath: '$.pythonVersions', fetch: mod9 } + : { kind: 'mirror', resourceType: 'pyenv', parameterPath: '$.pythonVersions', mirrorParameter: (mod9 as any).mirrorParameter }, + typeof mod10 === 'function' + ? { kind: 'fetch', resourceType: 'pyenv', parameterPath: '$.global', fetch: mod10 } + : { kind: 'mirror', resourceType: 'pyenv', parameterPath: '$.global', mirrorParameter: (mod10 as any).mirrorParameter }, + typeof mod11 === 'function' + ? { kind: 'fetch', resourceType: 'pip', parameterPath: '$.install', fetch: mod11 } + : { kind: 'mirror', resourceType: 'pip', parameterPath: '$.install', mirrorParameter: (mod11 as any).mirrorParameter }, + typeof mod12 === 'function' + ? { kind: 'fetch', resourceType: 'ollama', parameterPath: '$.models', fetch: mod12 } + : { kind: 'mirror', resourceType: 'ollama', parameterPath: '$.models', mirrorParameter: (mod12 as any).mirrorParameter }, + typeof mod13 === 'function' + ? { kind: 'fetch', resourceType: 'rustrover', parameterPath: '$.plugins', fetch: mod13 } + : { kind: 'mirror', resourceType: 'rustrover', parameterPath: '$.plugins', mirrorParameter: (mod13 as any).mirrorParameter }, + typeof mod14 === 'function' + ? { kind: 'fetch', resourceType: 'rubymine', parameterPath: '$.plugins', fetch: mod14 } + : { kind: 'mirror', resourceType: 'rubymine', parameterPath: '$.plugins', mirrorParameter: (mod14 as any).mirrorParameter }, + typeof mod15 === 'function' + ? { kind: 'fetch', resourceType: 'rider', parameterPath: '$.plugins', fetch: mod15 } + : { kind: 'mirror', resourceType: 'rider', parameterPath: '$.plugins', mirrorParameter: (mod15 as any).mirrorParameter }, + typeof mod16 === 'function' + ? { kind: 'fetch', resourceType: 'pycharm', parameterPath: '$.plugins', fetch: mod16 } + : { kind: 'mirror', resourceType: 'pycharm', parameterPath: '$.plugins', mirrorParameter: (mod16 as any).mirrorParameter }, + typeof mod17 === 'function' + ? { kind: 'fetch', resourceType: 'phpstorm', parameterPath: '$.plugins', fetch: mod17 } + : { kind: 'mirror', resourceType: 'phpstorm', parameterPath: '$.plugins', mirrorParameter: (mod17 as any).mirrorParameter }, + typeof mod18 === 'function' + ? { kind: 'fetch', resourceType: 'intellij-idea', parameterPath: '$.plugins', fetch: mod18 } + : { kind: 'mirror', resourceType: 'intellij-idea', parameterPath: '$.plugins', mirrorParameter: (mod18 as any).mirrorParameter }, + typeof mod19 === 'function' + ? { kind: 'fetch', resourceType: 'goland', parameterPath: '$.plugins', fetch: mod19 } + : { kind: 'mirror', resourceType: 'goland', parameterPath: '$.plugins', mirrorParameter: (mod19 as any).mirrorParameter }, + typeof mod20 === 'function' + ? { kind: 'fetch', resourceType: 'clion', parameterPath: '$.plugins', fetch: mod20 } + : { kind: 'mirror', resourceType: 'clion', parameterPath: '$.plugins', mirrorParameter: (mod20 as any).mirrorParameter }, + typeof mod21 === 'function' + ? { kind: 'fetch', resourceType: 'pnpm', parameterPath: '$.globalEnvNodeVersion', fetch: mod21 } + : { kind: 'mirror', resourceType: 'pnpm', parameterPath: '$.globalEnvNodeVersion', mirrorParameter: (mod21 as any).mirrorParameter }, + typeof mod22 === 'function' + ? { kind: 'fetch', resourceType: 'nvm', parameterPath: '$.nodeVersions', fetch: mod22 } + : { kind: 'mirror', resourceType: 'nvm', parameterPath: '$.nodeVersions', mirrorParameter: (mod22 as any).mirrorParameter }, + typeof mod23 === 'function' + ? { kind: 'fetch', resourceType: 'nvm', parameterPath: '$.global', fetch: mod23 } + : { kind: 'mirror', resourceType: 'nvm', parameterPath: '$.global', mirrorParameter: (mod23 as any).mirrorParameter }, + typeof mod24 === 'function' + ? { kind: 'fetch', resourceType: 'npm', parameterPath: '$.install', fetch: mod24 } + : { kind: 'mirror', resourceType: 'npm', parameterPath: '$.install', mirrorParameter: (mod24 as any).mirrorParameter }, + typeof mod25 === 'function' + ? { kind: 'fetch', resourceType: 'fast-node-manager', parameterPath: '$.defaultVersion', fetch: mod25 } + : { kind: 'mirror', resourceType: 'fast-node-manager', parameterPath: '$.defaultVersion', mirrorParameter: (mod25 as any).mirrorParameter }, + typeof mod26 === 'function' + ? { kind: 'fetch', resourceType: 'jenv', parameterPath: '$.global', fetch: mod26 } + : { kind: 'mirror', resourceType: 'jenv', parameterPath: '$.global', mirrorParameter: (mod26 as any).mirrorParameter }, + typeof mod27 === 'function' + ? { kind: 'fetch', resourceType: 'ios-simulators', parameterPath: '$.simulators[*].runtime', fetch: mod27 } + : { kind: 'mirror', resourceType: 'ios-simulators', parameterPath: '$.simulators[*].runtime', mirrorParameter: (mod27 as any).mirrorParameter }, + typeof mod28 === 'function' + ? { kind: 'fetch', resourceType: 'ios-simulators', parameterPath: '$.simulators[*].deviceType', fetch: mod28 } + : { kind: 'mirror', resourceType: 'ios-simulators', parameterPath: '$.simulators[*].deviceType', mirrorParameter: (mod28 as any).mirrorParameter }, + typeof mod29 === 'function' + ? { kind: 'fetch', resourceType: 'homebrew', parameterPath: '$.formulae', fetch: mod29 } + : { kind: 'mirror', resourceType: 'homebrew', parameterPath: '$.formulae', mirrorParameter: (mod29 as any).mirrorParameter }, + typeof mod30 === 'function' + ? { kind: 'fetch', resourceType: 'homebrew', parameterPath: '$.casks', fetch: mod30 } + : { kind: 'mirror', resourceType: 'homebrew', parameterPath: '$.casks', mirrorParameter: (mod30 as any).mirrorParameter }, + typeof mod31 === 'function' + ? { kind: 'fetch', resourceType: 'goenv', parameterPath: '$.global', fetch: mod31 } + : { kind: 'mirror', resourceType: 'goenv', parameterPath: '$.global', mirrorParameter: (mod31 as any).mirrorParameter }, + typeof mod32 === 'function' + ? { kind: 'fetch', resourceType: 'cursor', parameterPath: '$.extensions', fetch: mod32 } + : { kind: 'mirror', resourceType: 'cursor', parameterPath: '$.extensions', mirrorParameter: (mod32 as any).mirrorParameter }, + typeof mod33 === 'function' + ? { kind: 'fetch', resourceType: 'codex', parameterPath: '$.config.model', fetch: mod33 } + : { kind: 'mirror', resourceType: 'codex', parameterPath: '$.config.model', mirrorParameter: (mod33 as any).mirrorParameter }, + typeof mod34 === 'function' + ? { kind: 'fetch', resourceType: 'asdf', parameterPath: '$.plugins', fetch: mod34 } + : { kind: 'mirror', resourceType: 'asdf', parameterPath: '$.plugins', mirrorParameter: (mod34 as any).mirrorParameter }, + typeof mod35 === 'function' + ? { kind: 'fetch', resourceType: 'asdf-plugin', parameterPath: '$.plugin', fetch: mod35 } + : { kind: 'mirror', resourceType: 'asdf-plugin', parameterPath: '$.plugin', mirrorParameter: (mod35 as any).mirrorParameter }, + typeof mod36 === 'function' + ? { kind: 'fetch', resourceType: 'apt', parameterPath: '$.install', fetch: mod36 } + : { kind: 'mirror', resourceType: 'apt', parameterPath: '$.install', mirrorParameter: (mod36 as any).mirrorParameter }, + typeof mod37 === 'function' + ? { kind: 'fetch', resourceType: 'android-studio', parameterPath: '$.version', fetch: mod37 } + : { kind: 'mirror', resourceType: 'android-studio', parameterPath: '$.version', mirrorParameter: (mod37 as any).mirrorParameter }, + typeof mod38 === 'function' + ? { kind: 'fetch', resourceType: 'android-cli', parameterPath: '$.sdkPackages', fetch: mod38 } + : { kind: 'mirror', resourceType: 'android-cli', parameterPath: '$.sdkPackages', mirrorParameter: (mod38 as any).mirrorParameter }, + typeof mod39 === 'function' + ? { kind: 'fetch', resourceType: 'android-cli', parameterPath: '$.emulators', fetch: mod39 } + : { kind: 'mirror', resourceType: 'android-cli', parameterPath: '$.emulators', mirrorParameter: (mod39 as any).mirrorParameter }, ] diff --git a/completions-cron/src/index.ts b/completions-cron/src/index.ts index 9c87babd..bca330b3 100644 --- a/completions-cron/src/index.ts +++ b/completions-cron/src/index.ts @@ -28,7 +28,7 @@ async function getResourceId( return data[0].id } -async function processModule( +async function processFetchModule( supabase: SupabaseClient, resourceType: string, parameterPath: string, @@ -36,10 +36,10 @@ async function processModule( prerelease: boolean, resourceIdCache: Map ): Promise { - console.log(`Processing ${resourceType}${parameterPath}...`) + console.log(`Processing ${resourceType} → ${parameterPath}...`) const values = await fetchFn() - console.log(` [${resourceType}${parameterPath}] Fetched ${values.length} values`) + console.log(` [${resourceType} → ${parameterPath}] Fetched ${values.length} values`) const resourceId = await getResourceId(supabase, resourceType, prerelease, resourceIdCache) @@ -63,11 +63,47 @@ async function processModule( .insert(rows.slice(i, i + BATCH_SIZE)) if (error) { - throw new Error(`Insert failed for ${resourceType}${parameterPath}: ${error.message}`) + throw new Error(`Insert failed for ${resourceType} → ${parameterPath}: ${error.message}`) } } - console.log(` [${resourceType}${parameterPath}] Done: inserted ${values.length} completions`) + console.log(` [${resourceType} → ${parameterPath}] Done: inserted ${values.length} completions`) +} + +async function processMirrorModule( + supabase: SupabaseClient, + resourceType: string, + parameterPath: string, + mirrorParameter: string, + prerelease: boolean, + resourceIdCache: Map +): Promise { + console.log(`Processing mirror ${resourceType} → ${parameterPath} (mirrors ${mirrorParameter})...`) + + const resourceId = await getResourceId(supabase, resourceType, prerelease, resourceIdCache) + + // Delete any existing metadata row for this path (value IS NULL for mirror rows) + await supabase + .from('resource_parameter_completions') + .delete() + .eq('resource_type', resourceType) + .eq('resource_id', resourceId) + .eq('parameter_path', parameterPath) + + const { error } = await supabase + .from('resource_parameter_completions') + .insert({ + resource_type: resourceType, + resource_id: resourceId, + parameter_path: parameterPath, + mirror_parameter_path: mirrorParameter, + }) + + if (error) { + throw new Error(`Mirror insert failed for ${resourceType} → ${parameterPath}: ${error.message}`) + } + + console.log(` [${resourceType} → ${parameterPath}] Done: mirror metadata row written`) } async function runCompletions(env: Env): Promise { @@ -76,8 +112,10 @@ async function runCompletions(env: Env): Promise { const resourceIdCache = new Map() const results = await Promise.allSettled( - completionModules.map(({ resourceType, parameterPath, fetch }: CompletionModule) => - processModule(supabase, resourceType, parameterPath, fetch, prerelease, resourceIdCache) + completionModules.map((mod: CompletionModule) => + mod.kind === 'fetch' + ? processFetchModule(supabase, mod.resourceType, mod.parameterPath, mod.fetch, prerelease, resourceIdCache) + : processMirrorModule(supabase, mod.resourceType, mod.parameterPath, mod.mirrorParameter, prerelease, resourceIdCache) ) ) diff --git a/docs/resources/(resources)/ios-simulators.mdx b/docs/resources/(resources)/ios-simulators.mdx new file mode 100644 index 00000000..3032ffcc --- /dev/null +++ b/docs/resources/(resources)/ios-simulators.mdx @@ -0,0 +1,64 @@ +--- +title: ios-simulator +description: A reference page for the ios-simulator resource +--- + +The ios-simulator resource manages iOS (and iPadOS/watchOS/tvOS/visionOS) simulator instances on macOS +using `xcrun simctl`. A single resource declaration manages a list of simulators, making it easy to +define a full testing matrix across device types and OS versions in one place. Simulators are created with the specified device type and runtime. Removing the resource deletes all +declared simulators from the system. Xcode Command Line Tools must be installed — add an +`xcode-tools` resource as a dependency if you are not sure they are present. + +## Parameters: + +- **simulators** *(object[], optional)* — List of simulators to create and manage. Each entry has: + - **name** *(string, required)* — Human-readable name for the simulator instance (e.g. `"iPhone 15 Dev"`). Must be unique across your declared simulators. + - **deviceType** *(string, required)* — CoreSimulator device type identifier. Use the format `com.apple.CoreSimulator.SimDeviceType.`. Run `xcrun simctl list devicetypes` to see identifiers available on your machine. + - **runtime** *(string, required)* — CoreSimulator runtime identifier. Use the format `com.apple.CoreSimulator.SimRuntime.-`. Run `xcrun simctl list runtimes` to see installed runtimes. + + +## Example usage: + +```json title="codify.jsonc" +[ + { + "type": "ios-simulators", + "simulators": [ + { + "name": "iPhone 15 Dev", + "deviceType": "com.apple.CoreSimulator.SimDeviceType.iPhone-15", + "runtime": "com.apple.CoreSimulator.SimRuntime.iOS-18-0", + + } + ], + "os": ["macOS"] + } +] +``` + +```json title="codify.jsonc" +[ + { + "type": "xcode-tools", + "os": ["macOS"] + }, + { + "type": "ios-simulators", + "simulators": [ + { + "name": "iPhone 15 Pro", + "deviceType": "com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro", + "runtime": "com.apple.CoreSimulator.SimRuntime.iOS-18-0", + + }, + { + "name": "iPad Pro 11-inch", + "deviceType": "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M4", + "runtime": "com.apple.CoreSimulator.SimRuntime.iOS-18-0", + + } + ], + "os": ["macOS"] + } +] +``` diff --git a/package.json b/package.json index aa3a8119..7ec93aed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "default", - "version": "1.14.0", + "version": "1.15.0-beta.6", "description": "Default plugin for Codify - provides 50+ declarative resources for managing development tools and system configuration across macOS and Linux", "main": "dist/index.js", "scripts": { diff --git a/scripts/generate-completions-index.ts b/scripts/generate-completions-index.ts index 14630f5f..c26d2715 100644 --- a/scripts/generate-completions-index.ts +++ b/scripts/generate-completions-index.ts @@ -24,11 +24,12 @@ const modules = completionFiles.map((relPath, i) => { const dotIndex = filename.indexOf('.') if (dotIndex === -1) { throw new Error( - `Completion file must be named ..ts, got: ${filename}` + `Completion file must be named ..ts (e.g. homebrew.$.formulae.ts, ios-simulators.$.simulators[x].deviceType.ts), got: ${filename}` ) } const resourceType = filename.substring(0, dotIndex) - const parameterPath = '/' + filename.substring(dotIndex + 1).replaceAll('.', '/') + // [x] in filenames encodes [*] (glob-safe); restore to proper JSONPath wildcard + const parameterPath = filename.substring(dotIndex + 1).replaceAll('[x]', '[*]') // Path from completions-cron/src/__generated__/ back to plugin src/resources/ const importPath = '../../../src/' + relPath.replace(/\.ts$/, '.js') @@ -42,15 +43,16 @@ for (const { importName, importPath } of modules) { } lines.push('') -lines.push('export interface CompletionModule {') -lines.push(' resourceType: string') -lines.push(' parameterPath: string') -lines.push(' fetch: () => Promise') -lines.push('}') +lines.push('export type CompletionModule =') +lines.push(' | { kind: \'fetch\'; resourceType: string; parameterPath: string; fetch: () => Promise }') +lines.push(' | { kind: \'mirror\'; resourceType: string; parameterPath: string; mirrorParameter: string }') lines.push('') lines.push('export const completionModules: CompletionModule[] = [') for (const { importName, resourceType, parameterPath } of modules) { - lines.push(` { resourceType: '${resourceType}', parameterPath: '${parameterPath}', fetch: ${importName} },`) + // A mirror module exports { mirrorParameter: '...' }, a fetch module exports a function. + lines.push(` typeof ${importName} === 'function'`) + lines.push(` ? { kind: 'fetch', resourceType: '${resourceType}', parameterPath: '${parameterPath}', fetch: ${importName} }`) + lines.push(` : { kind: 'mirror', resourceType: '${resourceType}', parameterPath: '${parameterPath}', mirrorParameter: (${importName} as any).mirrorParameter },`) } lines.push(']') lines.push('') @@ -59,5 +61,5 @@ fs.writeFileSync(outputFile, lines.join('\n'), 'utf-8') console.log(`Generated ${outputFile} with ${modules.length} completion module(s):`) for (const { resourceType, parameterPath } of modules) { - console.log(` ${resourceType}${parameterPath}`) + console.log(` ${resourceType} → ${parameterPath}`) } diff --git a/src/index.ts b/src/index.ts index a9106dd0..e2e6a0aa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -67,6 +67,7 @@ import { TerraformResource } from './resources/terraform/terraform.js'; import { CursorResource } from './resources/cursor/cursor.js'; import { VscodeResource } from './resources/vscode/vscode.js'; import { WebStormResource } from './resources/webstorm/webstorm.js'; +import { IosSimulatorResource } from './resources/ios/ios-simulator/ios-simulator.js'; import { XcodeToolsResource } from './resources/xcode-tools/xcode-tools.js'; import { XcodesResource } from './resources/xcodes/xcodes-resource.js'; import { YumResource } from './resources/yum/yum.js'; @@ -87,6 +88,7 @@ runPlugin(Plugin.create( [ new GitResource(), new XcodeToolsResource(), + new IosSimulatorResource(), new XcodesResource(), new PathResource(), new AliasResource(), diff --git a/src/resources/android/android-cli/completions/android-cli.emulators.ts b/src/resources/android/android-cli/completions/android-cli.$.emulators.ts similarity index 100% rename from src/resources/android/android-cli/completions/android-cli.emulators.ts rename to src/resources/android/android-cli/completions/android-cli.$.emulators.ts diff --git a/src/resources/android/android-cli/completions/android-cli.sdkPackages.ts b/src/resources/android/android-cli/completions/android-cli.$.sdkPackages.ts similarity index 100% rename from src/resources/android/android-cli/completions/android-cli.sdkPackages.ts rename to src/resources/android/android-cli/completions/android-cli.$.sdkPackages.ts diff --git a/src/resources/android/android-studios/completions/android-studio.version.ts b/src/resources/android/android-studios/completions/android-studio.$.version.ts similarity index 100% rename from src/resources/android/android-studios/completions/android-studio.version.ts rename to src/resources/android/android-studios/completions/android-studio.$.version.ts diff --git a/src/resources/apt/completions/apt.install.ts b/src/resources/apt/completions/apt.$.install.ts similarity index 100% rename from src/resources/apt/completions/apt.install.ts rename to src/resources/apt/completions/apt.$.install.ts diff --git a/src/resources/asdf/completions/asdf-plugin.plugin.ts b/src/resources/asdf/completions/asdf-plugin.$.plugin.ts similarity index 100% rename from src/resources/asdf/completions/asdf-plugin.plugin.ts rename to src/resources/asdf/completions/asdf-plugin.$.plugin.ts diff --git a/src/resources/asdf/completions/asdf.plugins.ts b/src/resources/asdf/completions/asdf.$.plugins.ts similarity index 100% rename from src/resources/asdf/completions/asdf.plugins.ts rename to src/resources/asdf/completions/asdf.$.plugins.ts diff --git a/src/resources/codex/completions/codex.config.model.ts b/src/resources/codex/completions/codex.$.config.model.ts similarity index 100% rename from src/resources/codex/completions/codex.config.model.ts rename to src/resources/codex/completions/codex.$.config.model.ts diff --git a/src/resources/cursor/completions/cursor.extensions.ts b/src/resources/cursor/completions/cursor.$.extensions.ts similarity index 100% rename from src/resources/cursor/completions/cursor.extensions.ts rename to src/resources/cursor/completions/cursor.$.extensions.ts diff --git a/src/resources/go/goenv/completions/goenv.$.global.ts b/src/resources/go/goenv/completions/goenv.$.global.ts new file mode 100644 index 00000000..1b00184c --- /dev/null +++ b/src/resources/go/goenv/completions/goenv.$.global.ts @@ -0,0 +1 @@ +export default { mirrorParameter: '$.goVersions' } as const; diff --git a/src/resources/homebrew/completions/homebrew.casks.ts b/src/resources/homebrew/completions/homebrew.$.casks.ts similarity index 100% rename from src/resources/homebrew/completions/homebrew.casks.ts rename to src/resources/homebrew/completions/homebrew.$.casks.ts diff --git a/src/resources/homebrew/completions/homebrew.formulae.ts b/src/resources/homebrew/completions/homebrew.$.formulae.ts similarity index 100% rename from src/resources/homebrew/completions/homebrew.formulae.ts rename to src/resources/homebrew/completions/homebrew.$.formulae.ts diff --git a/src/resources/ios/ios-simulator/completions/ios-simulators.$.simulators[x].deviceType.ts b/src/resources/ios/ios-simulator/completions/ios-simulators.$.simulators[x].deviceType.ts new file mode 100644 index 00000000..8635f10c --- /dev/null +++ b/src/resources/ios/ios-simulator/completions/ios-simulators.$.simulators[x].deviceType.ts @@ -0,0 +1,55 @@ +// Known CoreSimulator device type identifiers shipped with Xcode. +// These identifiers are stable across Xcode versions for each device family. +export default async function loadIosSimulatorDeviceTypes(): Promise { + return [ + // iPhone SE + 'com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation', + + // iPhone 14 family + 'com.apple.CoreSimulator.SimDeviceType.iPhone-14', + 'com.apple.CoreSimulator.SimDeviceType.iPhone-14-Plus', + 'com.apple.CoreSimulator.SimDeviceType.iPhone-14-Pro', + 'com.apple.CoreSimulator.SimDeviceType.iPhone-14-Pro-Max', + + // iPhone 15 family + 'com.apple.CoreSimulator.SimDeviceType.iPhone-15', + 'com.apple.CoreSimulator.SimDeviceType.iPhone-15-Plus', + 'com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro', + 'com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro-Max', + + // iPhone 16 family + 'com.apple.CoreSimulator.SimDeviceType.iPhone-16', + 'com.apple.CoreSimulator.SimDeviceType.iPhone-16-Plus', + 'com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro', + 'com.apple.CoreSimulator.SimDeviceType.iPhone-16-Pro-Max', + + // iPad mini + 'com.apple.CoreSimulator.SimDeviceType.iPad-mini-6th-generation', + 'com.apple.CoreSimulator.SimDeviceType.iPad-mini-A17-Pro', + + // iPad Air + 'com.apple.CoreSimulator.SimDeviceType.iPad-Air-5th-generation', + 'com.apple.CoreSimulator.SimDeviceType.iPad-Air-11-inch-M2', + 'com.apple.CoreSimulator.SimDeviceType.iPad-Air-13-inch-M2', + + // iPad Pro 11-inch + 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-4th-generation', + 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M4', + + // iPad Pro 12.9 / 13-inch + 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-12-9-inch-6th-generation', + 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-13-inch-M4', + + // Apple Watch + 'com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-9-41mm', + 'com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Series-9-45mm', + 'com.apple.CoreSimulator.SimDeviceType.Apple-Watch-Ultra-2-49mm', + + // Apple TV + 'com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-3rd-generation-4K', + 'com.apple.CoreSimulator.SimDeviceType.Apple-TV-4K-3rd-generation-1080p', + + // Apple Vision Pro + 'com.apple.CoreSimulator.SimDeviceType.Apple-Vision-Pro', + ]; +} diff --git a/src/resources/ios/ios-simulator/completions/ios-simulators.$.simulators[x].runtime.ts b/src/resources/ios/ios-simulator/completions/ios-simulators.$.simulators[x].runtime.ts new file mode 100644 index 00000000..c781be4c --- /dev/null +++ b/src/resources/ios/ios-simulator/completions/ios-simulators.$.simulators[x].runtime.ts @@ -0,0 +1,38 @@ +// Known CoreSimulator runtime identifiers. Each entry corresponds to an iOS +// (or watchOS/tvOS/visionOS) runtime that can be installed via Xcode. +export default async function loadIosSimulatorRuntimes(): Promise { + return [ + // iOS + 'com.apple.CoreSimulator.SimRuntime.iOS-16-0', + 'com.apple.CoreSimulator.SimRuntime.iOS-16-1', + 'com.apple.CoreSimulator.SimRuntime.iOS-16-2', + 'com.apple.CoreSimulator.SimRuntime.iOS-16-4', + 'com.apple.CoreSimulator.SimRuntime.iOS-17-0', + 'com.apple.CoreSimulator.SimRuntime.iOS-17-2', + 'com.apple.CoreSimulator.SimRuntime.iOS-17-4', + 'com.apple.CoreSimulator.SimRuntime.iOS-17-5', + 'com.apple.CoreSimulator.SimRuntime.iOS-18-0', + 'com.apple.CoreSimulator.SimRuntime.iOS-18-1', + 'com.apple.CoreSimulator.SimRuntime.iOS-18-2', + 'com.apple.CoreSimulator.SimRuntime.iOS-18-3', + 'com.apple.CoreSimulator.SimRuntime.iOS-18-4', + + // watchOS + 'com.apple.CoreSimulator.SimRuntime.watchOS-10-0', + 'com.apple.CoreSimulator.SimRuntime.watchOS-10-4', + 'com.apple.CoreSimulator.SimRuntime.watchOS-11-0', + 'com.apple.CoreSimulator.SimRuntime.watchOS-11-2', + + // tvOS + 'com.apple.CoreSimulator.SimRuntime.tvOS-17-0', + 'com.apple.CoreSimulator.SimRuntime.tvOS-17-4', + 'com.apple.CoreSimulator.SimRuntime.tvOS-18-0', + 'com.apple.CoreSimulator.SimRuntime.tvOS-18-2', + + // visionOS + 'com.apple.CoreSimulator.SimRuntime.xrOS-1-0', + 'com.apple.CoreSimulator.SimRuntime.xrOS-1-2', + 'com.apple.CoreSimulator.SimRuntime.xrOS-2-0', + 'com.apple.CoreSimulator.SimRuntime.xrOS-2-2', + ]; +} diff --git a/src/resources/ios/ios-simulator/ios-simulator.ts b/src/resources/ios/ios-simulator/ios-simulator.ts new file mode 100644 index 00000000..a8a68a48 --- /dev/null +++ b/src/resources/ios/ios-simulator/ios-simulator.ts @@ -0,0 +1,215 @@ +import { + CreatePlan, + DestroyPlan, + ExampleConfig, + ModifyPlan, + ParameterChange, + Resource, + ResourceSettings, + SpawnStatus, + getPty, + z, +} from '@codifycli/plugin-core'; +import { OS } from '@codifycli/schemas'; + +const simulatorSchema = z.object({ + name: z.string().describe('Name for the simulator instance (e.g. "iPhone 15 Dev")'), + deviceType: z.string().describe('Device type identifier (e.g. "com.apple.CoreSimulator.SimDeviceType.iPhone-15")'), + runtime: z.string().describe('Runtime identifier (e.g. "com.apple.CoreSimulator.SimRuntime.iOS-18-0")'), +}); + +export type SimulatorDeclaration = z.infer; + +const schema = z.object({ + simulators: z + .array(simulatorSchema) + .optional() + .describe('List of iOS simulators to create and manage.'), +}); + +export type IosSimulatorConfig = z.infer; + +interface SimDevice { + udid: string; + name: string; + state: string; + deviceTypeIdentifier: string; +} + +interface SimctlDevicesOutput { + devices: Record; +} + +const defaultConfig: Partial & { os: any } = { + simulators: [], + os: ['macOS'], +}; + +const exampleBasic: ExampleConfig = { + title: 'iPhone 15 simulator for development', + description: 'Create an iPhone 15 simulator running iOS 18 for use in development and UI testing.', + configs: [{ + type: 'ios-simulators', + simulators: [ + { + name: 'iPhone 15 Dev', + deviceType: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15', + runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-18-0', + }, + ], + os: ['macOS'], + }], +}; + +const exampleMultiDevice: ExampleConfig = { + title: 'iPhone and iPad simulator setup', + description: 'Install Xcode Command Line Tools and create an iPhone and iPad simulator for cross-device testing.', + configs: [ + { type: 'xcode-tools', os: ['macOS'] }, + { + type: 'ios-simulators', + simulators: [ + { + name: 'iPhone 15 Pro', + deviceType: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15-Pro', + runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-18-0', + }, + { + name: 'iPad Pro 11-inch', + deviceType: 'com.apple.CoreSimulator.SimDeviceType.iPad-Pro-11-inch-M4', + runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-18-0', + }, + ], + os: ['macOS'], + }, + ], +}; + +export class IosSimulatorResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'ios-simulators', + defaultConfig, + exampleConfigs: { + example1: exampleBasic, + example2: exampleMultiDevice, + }, + operatingSystems: [OS.Darwin], + dependencies: ['xcode-tools'], + schema, + parameterSettings: { + simulators: { + type: 'array', + itemType: 'object', + isElementEqual: (a, b) => + a.name === b.name && + a.deviceType === b.deviceType && + a.runtime === b.runtime, + filterInStatelessMode: (desired, current) => + current.filter((c) => desired.some((d) => d.name === c.name)), + canModify: true, + }, + }, + }; + } + + async refresh(): Promise | null> { + const allDevices = await this.listAllDevices(); + if (!allDevices) return null; + + const simulators: SimulatorDeclaration[] = []; + for (const [runtimeId, devices] of Object.entries(allDevices)) { + for (const device of devices) { + simulators.push({ + name: device.name, + deviceType: device.deviceTypeIdentifier, + runtime: runtimeId, + }); + } + } + + return simulators.length > 0 ? { simulators } : null; + } + + async create(plan: CreatePlan): Promise { + await this.assertSimctlAvailable(); + const $ = getPty(); + for (const sim of plan.desiredConfig.simulators ?? []) { + await $.spawn( + `xcrun simctl create "${sim.name}" "${sim.deviceType}" "${sim.runtime}"`, + { interactive: true }, + ); + } + } + + async modify(pc: ParameterChange, _plan: ModifyPlan): Promise { + if (pc.name !== 'simulators') return; + + const $ = getPty(); + const allDevices = await this.listAllDevices(); + if (!allDevices) return; + + const findUdid = (name: string): string | undefined => { + for (const devices of Object.values(allDevices)) { + const match = devices.find((d) => d.name === name); + if (match) return match.udid; + } + }; + + const previous: SimulatorDeclaration[] = pc.previousValue ?? []; + const desired: SimulatorDeclaration[] = pc.newValue ?? []; + + const toRemove = previous.filter((p) => !desired.some((d) => d.name === p.name)); + for (const sim of toRemove) { + const udid = findUdid(sim.name); + if (udid) await $.spawn(`xcrun simctl delete "${udid}"`, { interactive: true }); + } + + const toAdd = desired.filter((d) => !previous.some((p) => p.name === d.name)); + for (const sim of toAdd) { + await $.spawn( + `xcrun simctl create "${sim.name}" "${sim.deviceType}" "${sim.runtime}"`, + { interactive: true }, + ); + } + } + + async destroy(plan: DestroyPlan): Promise { + const $ = getPty(); + const allDevices = await this.listAllDevices(); + if (!allDevices) return; + + for (const sim of plan.currentConfig.simulators ?? []) { + for (const devices of Object.values(allDevices)) { + const match = devices.find((d) => d.name === sim.name); + if (match) { + await $.spawn(`xcrun simctl delete "${match.udid}"`, { interactive: true }); + break; + } + } + } + } + + private async assertSimctlAvailable(): Promise { + const $ = getPty(); + const { status } = await $.spawnSafe('xcrun simctl help'); + if (status !== SpawnStatus.SUCCESS) { + throw new Error( + 'xcrun simctl is not available. Xcode must be installed to manage iOS simulators. ' + + 'Install it manually from the Mac App Store or use the xcodes resource to manage Xcode versions.', + ); + } + } + + private async listAllDevices(): Promise | null> { + const $ = getPty(); + const { status, data } = await $.spawnSafe('xcrun simctl list devices --json'); + if (status !== SpawnStatus.SUCCESS) return null; + try { + const parsed: SimctlDevicesOutput = JSON.parse(data); + return parsed.devices; + } catch { + return null; + } + } +} diff --git a/src/resources/java/jenv/completions/jenv.$.global.ts b/src/resources/java/jenv/completions/jenv.$.global.ts new file mode 100644 index 00000000..673cadc1 --- /dev/null +++ b/src/resources/java/jenv/completions/jenv.$.global.ts @@ -0,0 +1 @@ +export default { mirrorParameter: '$.add' } as const; diff --git a/src/resources/javascript/fast-node-manager/completions/fast-node-manager.$.defaultVersion.ts b/src/resources/javascript/fast-node-manager/completions/fast-node-manager.$.defaultVersion.ts new file mode 100644 index 00000000..103d6901 --- /dev/null +++ b/src/resources/javascript/fast-node-manager/completions/fast-node-manager.$.defaultVersion.ts @@ -0,0 +1 @@ +export default { mirrorParameter: '$.nodeVersions' } as const; diff --git a/src/resources/javascript/npm/completions/npm.install.ts b/src/resources/javascript/npm/completions/npm.$.install.ts similarity index 100% rename from src/resources/javascript/npm/completions/npm.install.ts rename to src/resources/javascript/npm/completions/npm.$.install.ts diff --git a/src/resources/javascript/nvm/completions/nvm.$.global.ts b/src/resources/javascript/nvm/completions/nvm.$.global.ts new file mode 100644 index 00000000..103d6901 --- /dev/null +++ b/src/resources/javascript/nvm/completions/nvm.$.global.ts @@ -0,0 +1 @@ +export default { mirrorParameter: '$.nodeVersions' } as const; diff --git a/src/resources/javascript/nvm/completions/nvm.nodeVersions.ts b/src/resources/javascript/nvm/completions/nvm.$.nodeVersions.ts similarity index 100% rename from src/resources/javascript/nvm/completions/nvm.nodeVersions.ts rename to src/resources/javascript/nvm/completions/nvm.$.nodeVersions.ts diff --git a/src/resources/javascript/pnpm/completions/pnpm.globalEnvNodeVersion.ts b/src/resources/javascript/pnpm/completions/pnpm.$.globalEnvNodeVersion.ts similarity index 100% rename from src/resources/javascript/pnpm/completions/pnpm.globalEnvNodeVersion.ts rename to src/resources/javascript/pnpm/completions/pnpm.$.globalEnvNodeVersion.ts diff --git a/src/resources/jetbrains/clion/completions/clion.plugins.ts b/src/resources/jetbrains/clion/completions/clion.$.plugins.ts similarity index 100% rename from src/resources/jetbrains/clion/completions/clion.plugins.ts rename to src/resources/jetbrains/clion/completions/clion.$.plugins.ts diff --git a/src/resources/jetbrains/goland/completions/goland.plugins.ts b/src/resources/jetbrains/goland/completions/goland.$.plugins.ts similarity index 100% rename from src/resources/jetbrains/goland/completions/goland.plugins.ts rename to src/resources/jetbrains/goland/completions/goland.$.plugins.ts diff --git a/src/resources/jetbrains/intellij-idea/completions/intellij-idea.plugins.ts b/src/resources/jetbrains/intellij-idea/completions/intellij-idea.$.plugins.ts similarity index 100% rename from src/resources/jetbrains/intellij-idea/completions/intellij-idea.plugins.ts rename to src/resources/jetbrains/intellij-idea/completions/intellij-idea.$.plugins.ts diff --git a/src/resources/jetbrains/phpstorm/completions/phpstorm.plugins.ts b/src/resources/jetbrains/phpstorm/completions/phpstorm.$.plugins.ts similarity index 100% rename from src/resources/jetbrains/phpstorm/completions/phpstorm.plugins.ts rename to src/resources/jetbrains/phpstorm/completions/phpstorm.$.plugins.ts diff --git a/src/resources/jetbrains/pycharm/completions/pycharm.plugins.ts b/src/resources/jetbrains/pycharm/completions/pycharm.$.plugins.ts similarity index 100% rename from src/resources/jetbrains/pycharm/completions/pycharm.plugins.ts rename to src/resources/jetbrains/pycharm/completions/pycharm.$.plugins.ts diff --git a/src/resources/jetbrains/rider/completions/rider.plugins.ts b/src/resources/jetbrains/rider/completions/rider.$.plugins.ts similarity index 100% rename from src/resources/jetbrains/rider/completions/rider.plugins.ts rename to src/resources/jetbrains/rider/completions/rider.$.plugins.ts diff --git a/src/resources/jetbrains/rubymine/completions/rubymine.plugins.ts b/src/resources/jetbrains/rubymine/completions/rubymine.$.plugins.ts similarity index 100% rename from src/resources/jetbrains/rubymine/completions/rubymine.plugins.ts rename to src/resources/jetbrains/rubymine/completions/rubymine.$.plugins.ts diff --git a/src/resources/jetbrains/rustrover/completions/rustrover.plugins.ts b/src/resources/jetbrains/rustrover/completions/rustrover.$.plugins.ts similarity index 100% rename from src/resources/jetbrains/rustrover/completions/rustrover.plugins.ts rename to src/resources/jetbrains/rustrover/completions/rustrover.$.plugins.ts diff --git a/src/resources/ollama/completions/ollama.models.ts b/src/resources/ollama/completions/ollama.$.models.ts similarity index 100% rename from src/resources/ollama/completions/ollama.models.ts rename to src/resources/ollama/completions/ollama.$.models.ts diff --git a/src/resources/python/pip/completions/pip.install.ts b/src/resources/python/pip/completions/pip.$.install.ts similarity index 100% rename from src/resources/python/pip/completions/pip.install.ts rename to src/resources/python/pip/completions/pip.$.install.ts diff --git a/src/resources/python/pyenv/completions/pyenv.$.global.ts b/src/resources/python/pyenv/completions/pyenv.$.global.ts new file mode 100644 index 00000000..d63f1999 --- /dev/null +++ b/src/resources/python/pyenv/completions/pyenv.$.global.ts @@ -0,0 +1 @@ +export default { mirrorParameter: '$.pythonVersions' } as const; diff --git a/src/resources/python/pyenv/completions/pyenv.pythonVersions.ts b/src/resources/python/pyenv/completions/pyenv.$.pythonVersions.ts similarity index 100% rename from src/resources/python/pyenv/completions/pyenv.pythonVersions.ts rename to src/resources/python/pyenv/completions/pyenv.$.pythonVersions.ts diff --git a/src/resources/python/uv/completions/uv.pythonVersions.ts b/src/resources/python/uv/completions/uv.$.pythonVersions.ts similarity index 100% rename from src/resources/python/uv/completions/uv.pythonVersions.ts rename to src/resources/python/uv/completions/uv.$.pythonVersions.ts diff --git a/src/resources/python/uv/completions/uv.tools.ts b/src/resources/python/uv/completions/uv.$.tools.ts similarity index 100% rename from src/resources/python/uv/completions/uv.tools.ts rename to src/resources/python/uv/completions/uv.$.tools.ts diff --git a/src/resources/ruby/rbenv/completions/rbenv.$.global.ts b/src/resources/ruby/rbenv/completions/rbenv.$.global.ts new file mode 100644 index 00000000..5cf00cc5 --- /dev/null +++ b/src/resources/ruby/rbenv/completions/rbenv.$.global.ts @@ -0,0 +1 @@ +export default { mirrorParameter: '$.rubyVersions' } as const; diff --git a/src/resources/ruby/rbenv/completions/rbenv.rubyVersions.ts b/src/resources/ruby/rbenv/completions/rbenv.$.rubyVersions.ts similarity index 100% rename from src/resources/ruby/rbenv/completions/rbenv.rubyVersions.ts rename to src/resources/ruby/rbenv/completions/rbenv.$.rubyVersions.ts diff --git a/src/resources/snap/completions/snap.install.ts b/src/resources/snap/completions/snap.$.install.ts similarity index 100% rename from src/resources/snap/completions/snap.install.ts rename to src/resources/snap/completions/snap.$.install.ts diff --git a/src/resources/vscode/completions/vscode.extensions.ts b/src/resources/vscode/completions/vscode.$.extensions.ts similarity index 100% rename from src/resources/vscode/completions/vscode.extensions.ts rename to src/resources/vscode/completions/vscode.$.extensions.ts diff --git a/src/resources/webstorm/completions/webstorm.plugins.ts b/src/resources/webstorm/completions/webstorm.$.plugins.ts similarity index 100% rename from src/resources/webstorm/completions/webstorm.plugins.ts rename to src/resources/webstorm/completions/webstorm.$.plugins.ts diff --git a/src/resources/xcodes/completions/xcodes.$.selected.ts b/src/resources/xcodes/completions/xcodes.$.selected.ts new file mode 100644 index 00000000..6687c92f --- /dev/null +++ b/src/resources/xcodes/completions/xcodes.$.selected.ts @@ -0,0 +1 @@ +export default { mirrorParameter: '$.xcodeVersions' } as const; diff --git a/src/resources/xcodes/completions/xcodes.xcodeVersions.ts b/src/resources/xcodes/completions/xcodes.$.xcodeVersions.ts similarity index 100% rename from src/resources/xcodes/completions/xcodes.xcodeVersions.ts rename to src/resources/xcodes/completions/xcodes.$.xcodeVersions.ts diff --git a/test/ios/ios-simulator.test.ts b/test/ios/ios-simulator.test.ts new file mode 100644 index 00000000..eb3ad4be --- /dev/null +++ b/test/ios/ios-simulator.test.ts @@ -0,0 +1,70 @@ +import { describe, expect, it } from 'vitest'; +import { PluginTester, testSpawn } from '@codifycli/plugin-test'; +import * as path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { SpawnStatus, Utils } from '@codifycli/plugin-core'; + +function isXcodeInstalled(): boolean { + const result = spawnSync('xcrun', ['simctl', 'help'], { stdio: 'ignore' }); + return result.status === 0; +} + +const skip = !Utils.isMacOS() || !isXcodeInstalled(); + +describe('iOS Simulator tests', { skip }, async () => { + const pluginPath = path.resolve('./src/index.ts'); + + it('Can create, add a simulator, and destroy iOS simulators', { timeout: 300000 }, async () => { + await PluginTester.fullTest(pluginPath, [ + { + type: 'ios-simulators', + simulators: [ + { + name: 'codify-test-iphone', + deviceType: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15', + runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-18-0', + }, + ], + }, + ], { + validateApply: async () => { + const { data, status } = await testSpawn('xcrun simctl list devices --json'); + expect(status).toBe(SpawnStatus.SUCCESS); + const parsed = JSON.parse(data); + const allDevices: any[] = Object.values(parsed.devices).flat(); + expect(allDevices.find((d: any) => d.name === 'codify-test-iphone')).toBeDefined(); + }, + testModify: { + modifiedConfigs: [{ + type: 'ios-simulators', + simulators: [ + { + name: 'codify-test-iphone', + deviceType: 'com.apple.CoreSimulator.SimDeviceType.iPhone-15', + runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-18-0', + }, + { + name: 'codify-test-ipad', + deviceType: 'com.apple.CoreSimulator.SimDeviceType.iPad-mini-6th-generation', + runtime: 'com.apple.CoreSimulator.SimRuntime.iOS-18-0', + }, + ], + }], + validateModify: async () => { + const { data } = await testSpawn('xcrun simctl list devices --json'); + const parsed = JSON.parse(data); + const allDevices: any[] = Object.values(parsed.devices).flat(); + expect(allDevices.find((d: any) => d.name === 'codify-test-iphone')).toBeDefined(); + expect(allDevices.find((d: any) => d.name === 'codify-test-ipad')).toBeDefined(); + }, + }, + validateDestroy: async () => { + const { data } = await testSpawn('xcrun simctl list devices --json'); + const parsed = JSON.parse(data); + const allDevices: any[] = Object.values(parsed.devices).flat(); + expect(allDevices.find((d: any) => d.name === 'codify-test-iphone')).toBeUndefined(); + expect(allDevices.find((d: any) => d.name === 'codify-test-ipad')).toBeUndefined(); + }, + }); + }); +});