← Todos os Posts

Migrações do SwiftData: leve ou personalizada, e quando você não precisa de um V2

A forma como o SwiftData trata a migração de esquemas é um avanço estrutural em relação ao Core Data, com uma armadilha na qual os times continuam caindo: declarar um novo VersionedSchema para mudanças que o SwiftData resolveria sozinho por meio de valores padrão declarados na própria propriedade. O resultado é um crash no dispositivo com a mensagem “Duplicate version checksums across stages detected”, mesmo com um código que parecia certo e compilava sem erros. O modelo de migração real do framework se apoia em três peças (VersionedSchema, MigrationStage, SchemaMigrationPlan) e em três tipos de migração: leve automática, leve declarada e personalizada1. A maioria das mudanças de esquema é automática. Algumas exigem uma etapa leve declarada. Uma pequena minoria exige uma etapa personalizada com as closures willMigrate e didMigrate.

Este artigo percorre o modelo de migração à luz da documentação da Apple, nomeia os casos cobertos por cada tipo de migração e trata da herança de classes do iOS 26, além de mostrar em que pé as betas do iOS 27 deixam as migrações. O enquadramento é “o que eu declaro contra o que o SwiftData resolve por mim”, porque é essa decisão que determina se a migração sai limpa ou se o app quebra na primeira execução. A pergunta complementar, como desenhar um esquema v1 para que essas migrações continuem baratas, é o assunto de O verdadeiro custo do SwiftData é a disciplina de esquema.

Resumo

  • As migrações do SwiftData combinam três protocolos: VersionedSchema (um retrato dos tipos de modelo em uma versão), MigrationStage (uma única transição de fromVersion para toVersion, com os casos .lightweight ou .custom) e SchemaMigrationPlan (a lista ordenada de etapas)1.
  • Adicionar uma propriedade @Model nova com valor padrão na própria declaração (var foo: Bool = false) não exige um VersionedSchema novo. O SwiftData resolve a adição automaticamente, como migração leve. Declarar um V2 para isso produz crashes de “Duplicate version checksums across stages detected”.
  • As migrações leves cobrem: adicionar, renomear e remover entidades, atributos e relacionamentos; mudar o tipo de um relacionamento; declarar @Attribute(originalName:) para acompanhar renomeações; especificar regras de exclusão. A maioria das mudanças de esquema cabe aqui.
  • As migrações personalizadas (MigrationStage.custom(fromVersion:toVersion:willMigrate:didMigrate:)) cobrem transformações de dados: dividir uma coluna em duas, calcular campos derivados, mover dados entre modelos. willMigrate recebe o contexto antigo; didMigrate, o novo.
  • O iOS 26 acrescenta herança de classes para os tipos @Model2. Esquemas que adotam herança sobem para uma versão nova com uma etapa leve a partir da versão anterior, de modelos planos.

O modelo de três peças

Uma migração do SwiftData é composta por três peças.

VersionedSchema

Um retrato dos tipos de modelo em uma versão específica do esquema1. O protocolo exige:

  • static var versionIdentifier: Schema.Version. Um trio de versão semântica (Schema.Version(1, 0, 0)).
  • static var models: [any PersistentModel.Type]. O array de tipos @Model presentes nesta versão.
enum SchemaV1: VersionedSchema {
    static let versionIdentifier = Schema.Version(1, 0, 0)
    static var models: [any PersistentModel.Type] {
        [Item.self]
    }

    @Model
    final class Item {
        var name: String
        var createdAt: Date
        init(name: String, createdAt: Date) {
            self.name = name
            self.createdAt = createdAt
        }
    }
}

O padrão de enum com tipos aninhados é a convenção adotada. Cada VersionedSchema dá um espaço de nomes próprio às suas classes de modelo, de modo que vários esquemas com o mesmo nome de modelo podem conviver no código durante uma migração.

MigrationStage

Uma única transição entre dois tipos VersionedSchema3. São dois casos:

  • .lightweight(fromVersion: any VersionedSchema.Type, toVersion: any VersionedSchema.Type). Declara uma transição que o SwiftData executa sem código do app. Os parâmetros são os próprios tipos VersionedSchema (por exemplo, SchemaV1.self), não valores Schema.Version crus.
  • .custom(fromVersion:toVersion:willMigrate:didMigrate:). Declara uma transição com código que roda antes da migração de dados, depois dela ou nos dois momentos. Os argumentos de versão têm os mesmos tipos que em .lightweight.

SchemaMigrationPlan

A lista ordenada de etapas que leva o esquema de qualquer versão anterior até a atual1.

enum AppMigrationPlan: SchemaMigrationPlan {
    static var schemas: [any VersionedSchema.Type] {
        [SchemaV1.self, SchemaV2.self, SchemaV3.self]
    }

    static var stages: [MigrationStage] {
        [migrateV1toV2, migrateV2toV3]
    }

    static let migrateV1toV2 = MigrationStage.lightweight(
        fromVersion: SchemaV1.self,
        toVersion: SchemaV2.self
    )

    static let migrateV2toV3 = MigrationStage.custom(
        fromVersion: SchemaV2.self,
        toVersion: SchemaV3.self,
        willMigrate: { context in
            // Pre-migration: read old data, prepare it
            try context.save()
        },
        didMigrate: { context in
            // Post-migration: backfill new fields
            let descriptor = FetchDescriptor<SchemaV3.Item>()
            let items = try context.fetch(descriptor)
            for item in items {
                item.computedField = computeFromExisting(item)
            }
            try context.save()
        }
    )
}

O ModelContainer é configurado com o esquema atual e com o plano de migração ao mesmo tempo:

let container = try ModelContainer(
    for: SchemaV3.Item.self,
    migrationPlan: AppMigrationPlan.self,
    configurations: ModelConfiguration(...)
)

Na criação do container, o SwiftData lê a versão de esquema atual do armazenamento persistente, percorre as etapas do plano dessa versão em diante até a atual e aplica cada etapa em ordem.

O que as migrações leves resolvem automaticamente

A maioria das mudanças de esquema não exige uma etapa personalizada1:

  • Adicionar um atributo com valor padrão. var foo: Bool = false em um @Model existente é automático.
  • Adicionar uma entidade nova (classe de modelo). Os tipos novos aparecem quando o VersionedSchema deles passa a ser o atual; os dados existentes são preservados.
  • Remover um atributo ou uma entidade. O SwiftData descarta a coluna ou a tabela.
  • Renomear um atributo ou uma entidade. Adicione @Attribute(originalName: "oldName") à propriedade para preservar os dados; o SwiftData mapeia o antigo para o novo.
  • Mudar o tipo de um relacionamento. De um-para-muitos para muitos-para-muitos e assim por diante.
  • Especificar regras de exclusão. @Relationship(deleteRule: .cascade) e acréscimos parecidos são leves.

Para as mudanças dessa lista, o padrão certo é não declarar nenhum VersionedSchema novo, desde que os tipos de modelo permaneçam iguais no restante. O SwiftData executa a migração leve automaticamente contra o esquema existente.

A armadilha: adicionar um campo não exige V2

O erro mais comum nas migrações do SwiftData: alguém adiciona uma propriedade nova com valor padrão na declaração (var foo: Bool = false) e em seguida declara um SchemaV2 que referencia os mesmos tipos de modelo do SchemaV1. O build sai limpo. A primeira execução em um dispositivo com dados V1 existentes quebra com Duplicate version checksums across stages detected, porque tanto SchemaV1 quanto SchemaV2 resolvem para o mesmo checksum (os tipos de modelo não mudaram de um jeito que o SwiftData perceba como diferente).

O padrão correto: deixe o VersionedSchema existente em paz, adicione a propriedade nova ao modelo com valor padrão na declaração e deixe a migração leve automática do SwiftData cuidar disso. Nenhum MigrationPlan, nenhuma MigrationStage, nenhum V2 necessário.

// V1 schema
enum SchemaV1: VersionedSchema {
    @Model
    final class Item {
        var name: String
        // BEFORE: just these two properties
        var createdAt: Date
        // AFTER: add a third with inline default
        var isFavorite: Bool = false   // Lightweight, automatic
    }
}

A mudança var isFavorite: Bool = false vai para produção sem nenhuma declaração de MigrationStage. O inicializador de ModelContainer que não passa migrationPlan: funciona:

let container = try ModelContainer(
    for: SchemaV1.Item.self,
    configurations: ModelConfiguration(...)
)

O esquema V2 só se torna obrigatório quando uma mudança não pode ser leve: uma transformação de dados, a divisão de um modelo, uma reestruturação por herança que exige lógica própria. Nesses casos, o V2 é real e um SchemaMigrationPlan orquestra a transição.

Quando as migrações personalizadas são necessárias

As migrações personalizadas pagam a própria complexidade em três casos.

1. Dividir um campo em vários. Um campo String que guarda "Last, First" vira dois campos, firstName e lastName. A migração precisa ler o valor antigo, interpretá-lo e escrever os campos novos.

static let migrateV1toV2 = MigrationStage.custom(
    fromVersion: SchemaV1.self,
    toVersion: SchemaV2.self,
    willMigrate: nil,
    didMigrate: { context in
        let descriptor = FetchDescriptor<SchemaV2.Person>()
        let people = try context.fetch(descriptor)
        for person in people {
            let parts = person.fullName.split(separator: ", ", maxSplits: 1)
            person.lastName = String(parts.first ?? "")
            person.firstName = String(parts.dropFirst().first ?? "")
        }
        try context.save()
    }
)

A closure didMigrate roda contra o contexto do esquema novo, então os campos novos estão acessíveis. A remoção do antigo fullName talvez precise esperar até que os campos novos estejam preenchidos; essa limpeza vira uma etapa posterior de V2 para V3.

2. Calcular campos derivados. Um @Attribute novo que depende de dados existentes precisa ser preenchido no momento da migração.

3. Mover dados entre modelos. Uma reorganização em que os dados de Item são divididos entre Item e um modelo Tag novo exige lógica própria para atribuir as tags a partir dos dados antigos.

O princípio geral: leve quando muda a forma do esquema; personalizada quando muda a forma dos dados.

willMigrate e didMigrate

As etapas personalizadas têm duas closures, chamadas em momentos diferentes4.

willMigrate roda antes de o SwiftData aplicar a migração de esquema. O contexto de modelo que a closure recebe é o do esquema antigo. Use esse contexto para capturar dados, desnormalizá-los ou preparar estado auxiliar antes que o esquema mude por baixo.

didMigrate roda depois da migração de esquema. O contexto de modelo é o do esquema novo. Use esse contexto para preencher campos novos, calcular dados derivados ou finalizar a migração.

Qualquer uma das closures pode ser nil se não for necessária. A maioria das migrações personalizadas usa só didMigrate; willMigrate é útil quando a migração precisa ler dados antigos que deixarão de ser acessíveis depois da mudança de esquema.

A closure recebe um ModelContext e pode consultar, alterar e salvar. Ela é declarada como throwing: os erros escapam da migração e a abortam.

iOS 26: herança de classes para @Model

O iOS 26 introduz herança de classes para os modelos do SwiftData2. Agora os modelos podem ter relações de pai e filho:

@Model
class Vehicle {
    var make: String
    var year: Int
    init(make: String, year: Int) {
        self.make = make
        self.year = year
    }
}

@Model
final class Car: Vehicle {
    var doorCount: Int
    init(make: String, year: Int, doorCount: Int) {
        self.doorCount = doorCount
        super.init(make: make, year: year)
    }
}

Esquemas que adotam herança sobem para uma versão nova com uma etapa de migração leve a partir da versão anterior, de modelos planos. A transição é automática se a herança preservar as propriedades existentes; campos novos na subclasse seguem o padrão habitual de valores padrão na declaração.

O padrão serve aos casos em que vários tipos @Model compartilham características: um pai Vehicle com os filhos Car, Truck e Motorcycle; um pai Account com os filhos CheckingAccount e SavingsAccount. As propriedades compartilhadas ficam no pai; as especificidades, nos filhos.

iOS 27: o modelo de migração se mantém, o armazenamento fica observável

As betas do iOS 27 não mudam nada na mecânica de migração em si. VersionedSchema, MigrationStage e SchemaMigrationPlan seguem inalterados, e todo padrão acima vale exatamente como está escrito. O que o iOS 27 acrescenta fica ao lado das migrações, não dentro delas: uma nova superfície de “Data store observation” com dois tipos, ResultsObserver e HistoryObserver, além de uma opção @Attribute(.codable) que guarda uma propriedade por meio da representação Codable dela6.

Duas dessas adições merecem menção em um guia de migrações.

@Attribute(.codable) reduz a pressão de migração futura. Um tipo de valor Codable guardado de forma declarativa significa menos situações em que você teria de achatar uma struct em colunas paralelas e, depois, escrever uma etapa personalizada para remontá-la. Esquemas que adotam a opção em propriedades novas seguem as regras de valor padrão vistas antes: é uma opção de atributo, não uma mudança na forma do esquema6.

HistoryObserver fecha o ciclo depois de uma migração personalizada. Um preenchimento feito em didMigrate escreve linhas das quais o resto do app (e qualquer widget ou extensão que observe o armazenamento) precisa ficar sabendo. No iOS 27, um observador que acompanha o histórico persistente por meio de HistoryObserver vê as transações da migração chegarem e pode chamar ModelContext.fetchHistory para ler exatamente o que mudou, filtrado por tipo de modelo e autor da transação, em vez de recarregar tudo6. A história completa da observação está em SwiftData no iOS 27: observação e histórico.

A conclusão para o planejamento: nada nas betas do 27 obriga a subir a versão do esquema, e nenhum código de migração precisa ser reescrito. Adote os tipos novos de observação onde a sua lógica de reconciliação pós-migração costumava ficar sondando ou recarregando dados.

Testando migrações

Uma migração que compila não é uma migração pronta para ir ao ar. Três práticas de teste valem a pena antes do lançamento.

1. Teste de ida e volta em uma cópia do banco de produção. Pegue um banco recente com formato de produção (ou gere dados V1 sintéticos pelos testes), abra-o com o container que conhece o V2 e verifique se os dados migram corretamente. O teste pega bugs de migração personalizada que o verificador de tipos não enxerga.

2. A versão antiga ainda inicia. Compile a versão anterior do app, execute-a uma vez para produzir dados V1, depois compile a versão nova e confirme que ela inicia sem quebrar. O teste pega a armadilha de “Duplicate version checksums” e erros de declaração parecidos.

3. Recuperação de migração malsucedida. O que acontece se a migração lançar um erro? O comportamento do SwiftData depende da configuração do container; em apps de produção, um erro de migração não tratado não pode apagar em silêncio os dados do usuário. Teste o caminho de falha explicitamente e decida o que o app faz: reverter, avisar ou recuperar de um backup.

O artigo Uma única fonte de verdade, do mesmo conjunto, trata da questão vizinha do que acontece quando um armazenamento do SwiftData é substituído por sincronização entre processos. As migrações são o equivalente local desse padrão.

Entregar migrações entre processos e mostrar o progresso

Dois detalhes operacionais que a documentação não coloca em primeiro plano, mas que o time do SwiftData destacou na WWDC 20265: onde as migrações rodam quando um app tem widgets ou extensões, e como alimentar uma interface de progresso enquanto uma migração acontece.

Um único processo é dono da migração. Widgets e extensões não recebem os mesmos recursos de execução que o app principal, então não têm como realizar uma migração com segurança. A orientação é manter o SchemaMigrationPlan1 inteiramente fora dos targets de widget e de extensão e nunca migrar a partir deles. Escolha um processo, normalmente o app principal, como dono do banco. Se um widget abrir o container e o armazenamento em disco estiver em um esquema sem versão (mais antigo), a abertura falha. Trate esse erro como o sinal de que uma migração é necessária: mostre uma interface pedindo que a pessoa abra o app principal, deixe o app realizar a migração e gravar a versão migrada do esquema em um UserDefault compartilhado. O widget lê esse valor na vez seguinte e abre o container na versão para a qual o app já migrou. O padrão mantém um único escritor no comando e evita que dois processos disputem a evolução do mesmo arquivo.

O progresso é calculado pela contagem de etapas, não pelo tempo de relógio. O SwiftData não expõe nenhuma API dedicada ao progresso da migração5. Para alimentar um indicador de progresso, conte o total de etapas de migração personalizadas do plano e sobrescreva o handler didMigrate de cada etapa4 para que cada uma informe a própria posição: “etapa N de M”. O número reflete etapas concluídas, não tempo decorrido, então a barra avança em saltos discretos em vez de correr de forma contínua. A decisão de design que acompanha isso é o que o app mostra durante a migração: um spinner sozinho parece travamento e as pessoas desistem. Mantenha o app parcialmente utilizável onde os dados permitirem ou, no mínimo, descreva o que cada etapa acrescenta (os recursos novos que a migração destrava), para que a espera seja lida como progresso rumo a algo e não como tempo morto.

Modos de falha comuns

Três padrões tirados dos logs de erro do SwiftData.

Declarar um V2 para uma mudança que o SwiftData resolveria sozinho. O crash de “Duplicate version checksums”. Correção: não declare um esquema novo para adições de propriedades com valor padrão na declaração; deixe o SwiftData cuidar delas.

Código de migração personalizada que não salva. Uma closure didMigrate que altera entidades mas não chama context.save() produz uma migração que roda uma vez, joga fora o próprio trabalho e volta a rodar a cada inicialização (porque parece inacabada). Correção: toda closure que altera dados precisa chamar try context.save() antes de retornar.

Renomear uma propriedade sem @Attribute(originalName:). O SwiftData trata a propriedade nova como nova e a antiga como removida; os dados que estavam na propriedade antiga são descartados. Correção: declare @Attribute(originalName: "oldName") var newName: ... para que o SwiftData conduza os dados através da renomeação.

O que esse padrão significa para apps de iOS 26 em diante

Três conclusões.

  1. Por padrão, sem escada de VersionedSchema. Adicionar propriedades com valores padrão na declaração, apagar campos sem uso, renomear com @Attribute(originalName:): tudo leve e automático. A escada de VersionedSchema existe para mudanças que o SwiftData realmente não consegue resolver sozinho (transformações de dados, lógica própria, reestruturações por herança).

  2. Use MigrationStage.custom para transformações de dados, não para mudanças na forma do esquema. As closures willMigrate e didMigrate são para código que opera sobre dados, não para declarar que o esquema mudou. Mudanças de forma do esquema passam por etapas leves.

  3. Teste as migrações com dados V1 reais, não apenas com dados sintéticos. Migrações que passam em idas e voltas sintéticas ainda podem falhar com dados no formato de produção e seus casos extremos: campos nulos que o esquema não previu, conjuntos grandes que estouram o tempo limite e assim por diante. O custo de testar é pequeno; o custo de um crash de migração na primeira execução é bem real.

O conjunto completo sobre o ecossistema Apple: App Intents tipados; servidores MCP; a questão do roteamento; Foundation Models; a distinção entre LLM de execução e de ferramental; três superfícies; o padrão da fonte única de verdade; Dois servidores MCP; hooks para desenvolvimento Apple; Live Activities; o contrato de execução do watchOS; do que o SwiftUI é feito; o modelo mental espacial do RealityKit; a disciplina de esquema no SwiftData; os padrões de Liquid Glass; a entrega multiplataforma; a matriz de plataformas; o framework Vision; os Symbol Effects; a inferência com Core ML; a API de Writing Tools; Swift Testing; o Privacy Manifest a fundo; a acessibilidade como recurso de plataforma; a tipografia do SF Pro; os padrões espaciais do visionOS; o framework Speech; sobre o que me recuso a escrever. O ponto central é a série Ecossistema Apple. Para um contexto mais amplo de iOS com agentes de IA, veja o guia de desenvolvimento de agentes em iOS.

Perguntas frequentes

Sempre preciso de um SchemaMigrationPlan?

Não. Apps com uma única versão de esquema (o lançamento inicial, ou apps que só fizeram mudanças leves até agora) não precisam de SchemaMigrationPlan. O inicializador de ModelContainer aceita os modelos do esquema diretamente. O parâmetro migrationPlan: passa a ser necessário na primeira vez em que uma etapa de migração personalizada é declarada (ou na primeira vez em que você quiser declarar uma escada de versões explícita).

Como sei se a minha mudança é leve?

A lista da Apple de mudanças elegíveis à migração leve1: adicionar entidades, atributos e relacionamentos, removê-los, renomear com @Attribute(originalName:), mudar a cardinalidade de um relacionamento, especificar regras de exclusão. Se a mudança se encaixa em um desses casos e a estrutura das classes de modelo permanece igual no restante, a migração é automática e nenhuma escada de VersionedSchema é necessária. Se a mudança exige transformação de dados (calcular, dividir, mover), ela é personalizada.

willMigrate e didMigrate podem ser definidas juntas?

Sim. Cada closure é opcional separadamente, mas as duas podem ser fornecidas. willMigrate roda contra o contexto do esquema antigo antes de o SwiftData migrar; didMigrate roda contra o contexto do esquema novo depois. Juntas, cobrem preparação e finalização.

O que acontece se uma migração lançar um erro?

O erro escapa da inicialização do ModelContainer. O container não abre. O comportamento do app depende de como o erro é tratado: alguns apps exibem uma interface de recuperação, outros tentam restaurar de um backup, outros apagam o armazenamento corrompido e começam do zero. O SwiftData não apaga em silêncio os dados do usuário quando uma migração falha; tratar a falha cabe ao app.

Como testar uma migração sem afetar os dados de produção?

Monte um target de teste que crie um ModelContainer apontando para uma URL de arquivo temporário, preencha-o com dados V1 e depois o abra com o container novo que inclui o plano de migração. Verifique se os dados migrados correspondem ao esperado. O padrão funciona tanto em testes de unidade quanto de integração; para o resultado mais realista, use uma cópia de um banco real com formato de produção.

A herança de classes do iOS 26 funciona com esquemas existentes?

Sim, com uma migração leve. Apps que adotam herança sobem para uma versão nova de esquema (V4, por exemplo) e declaram um MigrationStage.lightweight(fromVersion: V3.self, toVersion: V4.self). As propriedades planas da classe pai permanecem, e as propriedades específicas da subclasse entram com valores padrão na declaração. A migração leve do SwiftData dá conta da mudança estrutural.

Referências


  1. Documentação da Apple Developer: referências dos protocolos VersionedSchema e SchemaMigrationPlan. O modelo de migração. Veja também o guia relacionado Adopting SwiftData for a Core Data app para a narrativa completa da evolução de esquema. 

  2. Apple Developer: SwiftData: Dive into inheritance and schema migration (WWDC 2025, sessão 291). A introdução da herança de classes do SwiftData no iOS 26. 

  3. Documentação da Apple Developer: MigrationStage, com os casos .lightweight(fromVersion:toVersion:) e .custom(fromVersion:toVersion:willMigrate:didMigrate:)

  4. Documentação da Apple Developer: MigrationStage.custom(fromVersion:toVersion:willMigrate:didMigrate:) para a assinatura do caso. A semântica de willMigrate rodar contra o contexto antigo e didMigrate contra o novo está documentada na sessão 291 da WWDC 2025, SwiftData: Dive into inheritance and schema migration, a mesma sessão citada para a adição de herança no iOS 26. 

  5. SwiftData Group Lab da WWDC 2026 (sessão 8017). Parafraseado de uma gravação do SwiftData Group Lab da WWDC 2026 transcrita localmente; a Apple não publica legendas oficiais dos labs. O critério de migração em widgets e extensões (um único processo é dono da migração, o caminho de erro é o sinal de migração, a versão migrada fica em um UserDefault) e a técnica de progresso por contagem de etapas (sobrescrever o handler didMigrate de cada etapa para informar a etapa N de M, já que não existe API de progresso dedicada) foram descritos pelo painel de engenharia do SwiftData. Os símbolos SchemaMigrationPlan e didMigrate de MigrationStage.custom estão confirmados na documentação da Apple Developer citada em 1 e 4; a ausência de uma API de progresso dedicada reflete o próprio enquadramento do painel durante o lab. 

  6. Documentação da Apple Developer: ResultsObserver e HistoryObserver (ambos em beta do iOS 27.0, sob o tópico “Data store observation” do SwiftData), e Schema.Attribute.Option.codable (beta do iOS 27.0), “uses the property’s codable representation to store the property”. Conforme a sessão 274 da WWDC26, What’s new in SwiftData, o HistoryObserver expõe um eventCounter observável que é incrementado quando novas transações chegam, e o código reage chamando ModelContext.fetchHistory com filtros de tipo de modelo e autor da transação. 

Artigos relacionados

O custo real do SwiftData é a disciplina de esquema

A API do SwiftData são duas macros. O custo aparece depois que você publica. Campos opcionais são a migração barata; adi…

20 min de leitura

SwiftData no iOS 27: Observation e History

O iOS 27 dá ao SwiftData observação de mudanças com ResultsObserver, histórico persistente com HistoryObserver e armazen…

12 min de leitura

Instalar e atualizar o Codex CLI: Mac, Linux, Windows

Todas as formas de instalar, atualizar, fixar versão e desinstalar a CLI do OpenAI Codex -- script de instalação, npm, H…

16 min de leitura