primitive-admin 1.1.0-alpha.9 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +523 -109
- package/assets/skill/skills/primitive-platform/SKILL.md +289 -0
- package/dist/bin/primitive.d.ts +2 -0
- package/dist/bin/primitive.js +372 -21
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.d.ts +2 -0
- package/dist/src/commands/admins.js +136 -30
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.d.ts +2 -0
- package/dist/src/commands/analytics.js +713 -55
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.d.ts +2 -0
- package/dist/src/commands/apps.js +65 -100
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth-sessions.d.ts +7 -0
- package/dist/src/commands/auth-sessions.js +144 -0
- package/dist/src/commands/auth-sessions.js.map +1 -0
- package/dist/src/commands/auth.d.ts +2 -0
- package/dist/src/commands/auth.js +238 -108
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/blob-buckets.d.ts +2 -0
- package/dist/src/commands/blob-buckets.js +331 -0
- package/dist/src/commands/blob-buckets.js.map +1 -0
- package/dist/src/commands/catalog.d.ts +2 -0
- package/dist/src/commands/catalog.js +63 -48
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/collection-type-configs.d.ts +2 -0
- package/dist/src/commands/collection-type-configs.js +85 -0
- package/dist/src/commands/collection-type-configs.js.map +1 -0
- package/dist/src/commands/collections.d.ts +2 -0
- package/dist/src/commands/collections.js +1282 -0
- package/dist/src/commands/collections.js.map +1 -0
- package/dist/src/commands/comparisons.d.ts +2 -0
- package/dist/src/commands/comparisons.js +6 -6
- package/dist/src/commands/comparisons.js.map +1 -1
- package/dist/src/commands/config.d.ts +46 -0
- package/dist/src/commands/config.js +465 -0
- package/dist/src/commands/config.js.map +1 -0
- package/dist/src/commands/connections.d.ts +2 -0
- package/dist/src/commands/connections.js +99 -0
- package/dist/src/commands/connections.js.map +1 -0
- package/dist/src/commands/cron-triggers.d.ts +2 -0
- package/dist/src/commands/cron-triggers.js +266 -0
- package/dist/src/commands/cron-triggers.js.map +1 -0
- package/dist/src/commands/database-type-configs.d.ts +2 -0
- package/dist/src/commands/database-type-configs.js +164 -0
- package/dist/src/commands/database-type-configs.js.map +1 -0
- package/dist/src/commands/database-types.d.ts +2 -0
- package/dist/src/commands/database-types.js +471 -0
- package/dist/src/commands/database-types.js.map +1 -0
- package/dist/src/commands/databases.d.ts +65 -0
- package/dist/src/commands/databases.js +1887 -243
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.d.ts +60 -0
- package/dist/src/commands/documents.js +2888 -22
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.d.ts +2 -0
- package/dist/src/commands/email-templates.js +175 -0
- package/dist/src/commands/email-templates.js.map +1 -0
- package/dist/src/commands/env.d.ts +23 -0
- package/dist/src/commands/env.js +395 -0
- package/dist/src/commands/env.js.map +1 -0
- package/dist/src/commands/feature-flags.d.ts +14 -0
- package/dist/src/commands/feature-flags.js +117 -0
- package/dist/src/commands/feature-flags.js.map +1 -0
- package/dist/src/commands/functions.d.ts +20 -0
- package/dist/src/commands/functions.js +1630 -0
- package/dist/src/commands/functions.js.map +1 -0
- package/dist/src/commands/group-type-configs.d.ts +2 -0
- package/dist/src/commands/group-type-configs.js +87 -0
- package/dist/src/commands/group-type-configs.js.map +1 -0
- package/dist/src/commands/groups.d.ts +2 -0
- package/dist/src/commands/groups.js +65 -113
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/guides.d.ts +223 -0
- package/dist/src/commands/guides.js +627 -69
- package/dist/src/commands/guides.js.map +1 -1
- package/dist/src/commands/init.d.ts +37 -0
- package/dist/src/commands/init.js +1693 -208
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.d.ts +2 -0
- package/dist/src/commands/integrations.js +428 -187
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/llm.d.ts +2 -0
- package/dist/src/commands/llm.js +4 -2
- package/dist/src/commands/llm.js.map +1 -1
- package/dist/src/commands/locks.d.ts +8 -0
- package/dist/src/commands/locks.js +175 -0
- package/dist/src/commands/locks.js.map +1 -0
- package/dist/src/commands/metadata-category-configs.d.ts +12 -0
- package/dist/src/commands/metadata-category-configs.js +113 -0
- package/dist/src/commands/metadata-category-configs.js.map +1 -0
- package/dist/src/commands/metadata.d.ts +2 -0
- package/dist/src/commands/metadata.js +288 -0
- package/dist/src/commands/metadata.js.map +1 -0
- package/dist/src/commands/prompts.d.ts +2 -0
- package/dist/src/commands/prompts.js +322 -634
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.d.ts +3 -0
- package/dist/src/commands/rule-sets.js +139 -148
- package/dist/src/commands/rule-sets.js.map +1 -1
- package/dist/src/commands/scripts.d.ts +30 -0
- package/dist/src/commands/scripts.js +688 -0
- package/dist/src/commands/scripts.js.map +1 -0
- package/dist/src/commands/secrets.d.ts +2 -0
- package/dist/src/commands/secrets.js +109 -0
- package/dist/src/commands/secrets.js.map +1 -0
- package/dist/src/commands/sessions.d.ts +2 -0
- package/dist/src/commands/sessions.js +76 -0
- package/dist/src/commands/sessions.js.map +1 -0
- package/dist/src/commands/skill.d.ts +2 -0
- package/dist/src/commands/skill.js +29 -0
- package/dist/src/commands/skill.js.map +1 -0
- package/dist/src/commands/sync-app-settings.d.ts +158 -0
- package/dist/src/commands/sync-app-settings.js +328 -0
- package/dist/src/commands/sync-app-settings.js.map +1 -0
- package/dist/src/commands/sync.d.ts +2670 -0
- package/dist/src/commands/sync.js +17144 -834
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.d.ts +2 -0
- package/dist/src/commands/tokens.js +132 -22
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.d.ts +2 -0
- package/dist/src/commands/users.js +542 -24
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/vars.d.ts +8 -0
- package/dist/src/commands/vars.js +97 -0
- package/dist/src/commands/vars.js.map +1 -0
- package/dist/src/commands/waitlist.d.ts +2 -0
- package/dist/src/commands/waitlist.js +12 -11
- package/dist/src/commands/waitlist.js.map +1 -1
- package/dist/src/commands/webhooks.d.ts +31 -0
- package/dist/src/commands/webhooks.js +633 -0
- package/dist/src/commands/webhooks.js.map +1 -0
- package/dist/src/commands/workflows.d.ts +88 -0
- package/dist/src/commands/workflows.js +1568 -742
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/access-rule-display.d.ts +21 -0
- package/dist/src/lib/access-rule-display.js +34 -0
- package/dist/src/lib/access-rule-display.js.map +1 -0
- package/dist/src/lib/api-client.d.ts +2550 -0
- package/dist/src/lib/api-client.js +2503 -160
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/app-settings-descriptor.d.ts +263 -0
- package/dist/src/lib/app-settings-descriptor.js +583 -0
- package/dist/src/lib/app-settings-descriptor.js.map +1 -0
- package/dist/src/lib/auth-flow.d.ts +8 -0
- package/dist/src/lib/batch.d.ts +26 -0
- package/dist/src/lib/batch.js +32 -0
- package/dist/src/lib/batch.js.map +1 -0
- package/dist/src/lib/block-layout.d.ts +160 -0
- package/dist/src/lib/block-layout.js +451 -0
- package/dist/src/lib/block-layout.js.map +1 -0
- package/dist/src/lib/block-selector.d.ts +58 -0
- package/dist/src/lib/block-selector.js +92 -0
- package/dist/src/lib/block-selector.js.map +1 -0
- package/dist/src/lib/canonical-json.d.ts +12 -0
- package/dist/src/lib/canonical-json.js +35 -0
- package/dist/src/lib/canonical-json.js.map +1 -0
- package/dist/src/lib/channel.d.ts +30 -0
- package/dist/src/lib/channel.js +68 -0
- package/dist/src/lib/channel.js.map +1 -0
- package/dist/src/lib/cli-manifest.d.ts +68 -0
- package/dist/src/lib/cli-manifest.js +71 -0
- package/dist/src/lib/cli-manifest.js.map +1 -0
- package/dist/src/lib/codegen-shared/generatedFiles.d.ts +101 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js +191 -0
- package/dist/src/lib/codegen-shared/generatedFiles.js.map +1 -0
- package/dist/src/lib/codegen-shared/prettierStable.d.ts +262 -0
- package/dist/src/lib/codegen-shared/prettierStable.js +610 -0
- package/dist/src/lib/codegen-shared/prettierStable.js.map +1 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.d.ts +38 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js +46 -0
- package/dist/src/lib/codegen-shared/resolveCodegenSourceDir.js.map +1 -0
- package/dist/src/lib/collection-export.d.ts +184 -0
- package/dist/src/lib/collection-export.js +252 -0
- package/dist/src/lib/collection-export.js.map +1 -0
- package/dist/src/lib/config-json-field.d.ts +28 -0
- package/dist/src/lib/config-json-field.js +56 -0
- package/dist/src/lib/config-json-field.js.map +1 -0
- package/dist/src/lib/config-object-descriptor.d.ts +127 -0
- package/dist/src/lib/config-object-descriptor.js +740 -0
- package/dist/src/lib/config-object-descriptor.js.map +1 -0
- package/dist/src/lib/config-payload.d.ts +92 -0
- package/dist/src/lib/config-payload.js +161 -0
- package/dist/src/lib/config-payload.js.map +1 -0
- package/dist/src/lib/config-surface.d.ts +141 -0
- package/dist/src/lib/config-surface.js +368 -0
- package/dist/src/lib/config-surface.js.map +1 -0
- package/dist/src/lib/config-toml.d.ts +10 -0
- package/dist/src/lib/config-toml.js +42 -0
- package/dist/src/lib/config-toml.js.map +1 -0
- package/dist/src/lib/config.d.ts +71 -0
- package/dist/src/lib/config.js +71 -68
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/confirm-prompt.d.ts +83 -0
- package/dist/src/lib/confirm-prompt.js +110 -0
- package/dist/src/lib/confirm-prompt.js.map +1 -0
- package/dist/src/lib/constants.d.ts +11 -0
- package/dist/src/lib/constants.js +12 -0
- package/dist/src/lib/constants.js.map +1 -0
- package/dist/src/lib/crash-handlers.d.ts +20 -0
- package/dist/src/lib/crash-handlers.js +49 -0
- package/dist/src/lib/crash-handlers.js.map +1 -0
- package/dist/src/lib/credentials-store.d.ts +104 -0
- package/dist/src/lib/credentials-store.js +336 -0
- package/dist/src/lib/credentials-store.js.map +1 -0
- package/dist/src/lib/csv.d.ts +47 -0
- package/dist/src/lib/csv.js +172 -0
- package/dist/src/lib/csv.js.map +1 -0
- package/dist/src/lib/data-input.d.ts +23 -0
- package/dist/src/lib/data-input.js +50 -0
- package/dist/src/lib/data-input.js.map +1 -0
- package/dist/src/lib/db-codegen/dbFingerprint.d.ts +10 -0
- package/dist/src/lib/db-codegen/dbFingerprint.js +17 -0
- package/dist/src/lib/db-codegen/dbFingerprint.js.map +1 -0
- package/dist/src/lib/db-codegen/dbGenerator.d.ts +67 -0
- package/dist/src/lib/db-codegen/dbGenerator.js +170 -0
- package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -0
- package/dist/src/lib/db-codegen/dbNaming.d.ts +87 -0
- package/dist/src/lib/db-codegen/dbNaming.js +180 -0
- package/dist/src/lib/db-codegen/dbNaming.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTemplates.d.ts +272 -0
- package/dist/src/lib/db-codegen/dbTemplates.js +480 -0
- package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTsTypes.d.ts +73 -0
- package/dist/src/lib/db-codegen/dbTsTypes.js +139 -0
- package/dist/src/lib/db-codegen/dbTsTypes.js.map +1 -0
- package/dist/src/lib/db-codegen/dbTypeIR.d.ts +146 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js +525 -0
- package/dist/src/lib/db-codegen/dbTypeIR.js.map +1 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.d.ts +112 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js +211 -0
- package/dist/src/lib/db-codegen/generated-operation-def-descriptor.js.map +1 -0
- package/dist/src/lib/deprecation.d.ts +22 -0
- package/dist/src/lib/deprecation.js +43 -0
- package/dist/src/lib/deprecation.js.map +1 -0
- package/dist/src/lib/document-export-permissions.d.ts +30 -0
- package/dist/src/lib/document-export-permissions.js +54 -0
- package/dist/src/lib/document-export-permissions.js.map +1 -0
- package/dist/src/lib/document-ingest-artifact.d.ts +120 -0
- package/dist/src/lib/document-ingest-artifact.js +505 -0
- package/dist/src/lib/document-ingest-artifact.js.map +1 -0
- package/dist/src/lib/document-ingest-input.d.ts +51 -0
- package/dist/src/lib/document-ingest-input.js +132 -0
- package/dist/src/lib/document-ingest-input.js.map +1 -0
- package/dist/src/lib/document-ingest-rows.d.ts +137 -0
- package/dist/src/lib/document-ingest-rows.js +181 -0
- package/dist/src/lib/document-ingest-rows.js.map +1 -0
- package/dist/src/lib/document-ingest.d.ts +101 -0
- package/dist/src/lib/document-ingest.js +384 -0
- package/dist/src/lib/document-ingest.js.map +1 -0
- package/dist/src/lib/env-resolver-core.d.ts +258 -0
- package/dist/src/lib/env-resolver-core.js +447 -0
- package/dist/src/lib/env-resolver-core.js.map +1 -0
- package/dist/src/lib/env-resolver.d.ts +99 -0
- package/dist/src/lib/env-resolver.js +153 -0
- package/dist/src/lib/env-resolver.js.map +1 -0
- package/dist/src/lib/fetch.d.ts +5 -0
- package/dist/src/lib/function-bundle.d.ts +147 -0
- package/dist/src/lib/function-bundle.js +341 -0
- package/dist/src/lib/function-bundle.js.map +1 -0
- package/dist/src/lib/function-collect.d.ts +123 -0
- package/dist/src/lib/function-collect.js +610 -0
- package/dist/src/lib/function-collect.js.map +1 -0
- package/dist/src/lib/function-db-types.d.ts +202 -0
- package/dist/src/lib/function-db-types.js +870 -0
- package/dist/src/lib/function-db-types.js.map +1 -0
- package/dist/src/lib/function-document-types.d.ts +144 -0
- package/dist/src/lib/function-document-types.js +370 -0
- package/dist/src/lib/function-document-types.js.map +1 -0
- package/dist/src/lib/function-grants-preflight.d.ts +64 -0
- package/dist/src/lib/function-grants-preflight.js +105 -0
- package/dist/src/lib/function-grants-preflight.js.map +1 -0
- package/dist/src/lib/function-log-lines.d.ts +76 -0
- package/dist/src/lib/function-log-lines.js +160 -0
- package/dist/src/lib/function-log-lines.js.map +1 -0
- package/dist/src/lib/function-log-row.d.ts +29 -0
- package/dist/src/lib/function-log-row.js +73 -0
- package/dist/src/lib/function-log-row.js.map +1 -0
- package/dist/src/lib/function-log-tail.d.ts +132 -0
- package/dist/src/lib/function-log-tail.js +262 -0
- package/dist/src/lib/function-log-tail.js.map +1 -0
- package/dist/src/lib/function-run.d.ts +289 -0
- package/dist/src/lib/function-run.js +389 -0
- package/dist/src/lib/function-run.js.map +1 -0
- package/dist/src/lib/function-schema-codegen.d.ts +143 -0
- package/dist/src/lib/function-schema-codegen.js +420 -0
- package/dist/src/lib/function-schema-codegen.js.map +1 -0
- package/dist/src/lib/function-sync.d.ts +459 -0
- package/dist/src/lib/function-sync.js +1258 -0
- package/dist/src/lib/function-sync.js.map +1 -0
- package/dist/src/lib/function-trigger-listing.d.ts +27 -0
- package/dist/src/lib/function-trigger-listing.js +70 -0
- package/dist/src/lib/function-trigger-listing.js.map +1 -0
- package/dist/src/lib/function-typecheck.d.ts +86 -0
- package/dist/src/lib/function-typecheck.js +370 -0
- package/dist/src/lib/function-typecheck.js.map +1 -0
- package/dist/src/lib/function-versions.d.ts +122 -0
- package/dist/src/lib/function-versions.js +182 -0
- package/dist/src/lib/function-versions.js.map +1 -0
- package/dist/src/lib/generated-allowlist.d.ts +28 -0
- package/dist/src/lib/generated-allowlist.js +281 -0
- package/dist/src/lib/generated-allowlist.js.map +1 -0
- package/dist/src/lib/generated-config-surfaces.d.ts +2936 -0
- package/dist/src/lib/generated-config-surfaces.js +10569 -0
- package/dist/src/lib/generated-config-surfaces.js.map +1 -0
- package/dist/src/lib/generated-sdk-types.d.ts +12 -0
- package/dist/src/lib/generated-sdk-types.js +13 -0
- package/dist/src/lib/generated-sdk-types.js.map +1 -0
- package/dist/src/lib/generated-template-lint.d.ts +212 -0
- package/dist/src/lib/generated-template-lint.js +624 -0
- package/dist/src/lib/generated-template-lint.js.map +1 -0
- package/dist/src/lib/init-adopt.d.ts +16 -0
- package/dist/src/lib/init-adopt.js +34 -0
- package/dist/src/lib/init-adopt.js.map +1 -0
- package/dist/src/lib/init-assets.d.ts +39 -0
- package/dist/src/lib/init-assets.js +97 -0
- package/dist/src/lib/init-assets.js.map +1 -0
- package/dist/src/lib/init-client-platforms.d.ts +14 -0
- package/dist/src/lib/init-client-platforms.js +71 -0
- package/dist/src/lib/init-client-platforms.js.map +1 -0
- package/dist/src/lib/init-config.d.ts +98 -0
- package/dist/src/lib/init-config.js +186 -0
- package/dist/src/lib/init-config.js.map +1 -0
- package/dist/src/lib/init-email-redirect-uris.d.ts +37 -0
- package/dist/src/lib/init-email-redirect-uris.js +46 -0
- package/dist/src/lib/init-email-redirect-uris.js.map +1 -0
- package/dist/src/lib/init-ios-links.d.ts +91 -0
- package/dist/src/lib/init-ios-links.js +219 -0
- package/dist/src/lib/init-ios-links.js.map +1 -0
- package/dist/src/lib/init-plan.d.ts +80 -0
- package/dist/src/lib/init-plan.js +95 -0
- package/dist/src/lib/init-plan.js.map +1 -0
- package/dist/src/lib/init-production-env.d.ts +48 -0
- package/dist/src/lib/init-production-env.js +59 -0
- package/dist/src/lib/init-production-env.js.map +1 -0
- package/dist/src/lib/init-schema.d.ts +74 -0
- package/dist/src/lib/init-schema.js +358 -0
- package/dist/src/lib/init-schema.js.map +1 -0
- package/dist/src/lib/init-xcode.d.ts +34 -0
- package/dist/src/lib/init-xcode.js +138 -0
- package/dist/src/lib/init-xcode.js.map +1 -0
- package/dist/src/lib/integration-request-config.d.ts +30 -0
- package/dist/src/lib/integration-request-config.js +145 -0
- package/dist/src/lib/integration-request-config.js.map +1 -0
- package/dist/src/lib/integration-selector.d.ts +42 -0
- package/dist/src/lib/integration-selector.js +46 -0
- package/dist/src/lib/integration-selector.js.map +1 -0
- package/dist/src/lib/ios-app-id.d.ts +34 -0
- package/dist/src/lib/ios-app-id.js +69 -0
- package/dist/src/lib/ios-app-id.js.map +1 -0
- package/dist/src/lib/list-options.d.ts +68 -0
- package/dist/src/lib/list-options.js +89 -0
- package/dist/src/lib/list-options.js.map +1 -0
- package/dist/src/lib/local-state.d.ts +55 -0
- package/dist/src/lib/local-state.js +167 -0
- package/dist/src/lib/local-state.js.map +1 -0
- package/dist/src/lib/local-test-cases.d.ts +63 -0
- package/dist/src/lib/local-test-cases.js +136 -0
- package/dist/src/lib/local-test-cases.js.map +1 -0
- package/dist/src/lib/log-inspection.d.ts +715 -0
- package/dist/src/lib/log-inspection.js +816 -0
- package/dist/src/lib/log-inspection.js.map +1 -0
- package/dist/src/lib/logout-admin-session.d.ts +33 -0
- package/dist/src/lib/logout-admin-session.js +70 -0
- package/dist/src/lib/logout-admin-session.js.map +1 -0
- package/dist/src/lib/migration-nag.d.ts +49 -0
- package/dist/src/lib/migration-nag.js +163 -0
- package/dist/src/lib/migration-nag.js.map +1 -0
- package/dist/src/lib/object-status-filter.d.ts +22 -0
- package/dist/src/lib/object-status-filter.js +45 -0
- package/dist/src/lib/object-status-filter.js.map +1 -0
- package/dist/src/lib/output.d.ts +124 -0
- package/dist/src/lib/output.js +219 -8
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/package-manager.d.ts +140 -0
- package/dist/src/lib/package-manager.js +305 -0
- package/dist/src/lib/package-manager.js.map +1 -0
- package/dist/src/lib/paginate.d.ts +98 -0
- package/dist/src/lib/paginate.js +112 -0
- package/dist/src/lib/paginate.js.map +1 -0
- package/dist/src/lib/platform-owned.d.ts +63 -0
- package/dist/src/lib/platform-owned.js +85 -0
- package/dist/src/lib/platform-owned.js.map +1 -0
- package/dist/src/lib/project-config.d.ts +122 -0
- package/dist/src/lib/project-config.js +244 -0
- package/dist/src/lib/project-config.js.map +1 -0
- package/dist/src/lib/prompt-cost-format.d.ts +11 -0
- package/dist/src/lib/prompt-cost-format.js +41 -0
- package/dist/src/lib/prompt-cost-format.js.map +1 -0
- package/dist/src/lib/prompt-schema-codegen.d.ts +147 -0
- package/dist/src/lib/prompt-schema-codegen.js +462 -0
- package/dist/src/lib/prompt-schema-codegen.js.map +1 -0
- package/dist/src/lib/query-operators.d.ts +43 -0
- package/dist/src/lib/query-operators.js +80 -0
- package/dist/src/lib/query-operators.js.map +1 -0
- package/dist/src/lib/record-filter.d.ts +18 -0
- package/dist/src/lib/record-filter.js +55 -0
- package/dist/src/lib/record-filter.js.map +1 -0
- package/dist/src/lib/refresh-admin-credentials.d.ts +73 -0
- package/dist/src/lib/refresh-admin-credentials.js +123 -0
- package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
- package/dist/src/lib/resolve-init-dev-port.d.ts +56 -0
- package/dist/src/lib/resolve-init-dev-port.js +55 -0
- package/dist/src/lib/resolve-init-dev-port.js.map +1 -0
- package/dist/src/lib/resolve-init-server.d.ts +64 -0
- package/dist/src/lib/resolve-init-server.js +77 -0
- package/dist/src/lib/resolve-init-server.js.map +1 -0
- package/dist/src/lib/resolve-owner.d.ts +19 -0
- package/dist/src/lib/resolve-owner.js +20 -0
- package/dist/src/lib/resolve-owner.js.map +1 -0
- package/dist/src/lib/resolve-platform.d.ts +74 -0
- package/dist/src/lib/resolve-platform.js +105 -0
- package/dist/src/lib/resolve-platform.js.map +1 -0
- package/dist/src/lib/run-status.d.ts +19 -0
- package/dist/src/lib/run-status.generated.d.ts +39 -0
- package/dist/src/lib/run-status.generated.js +66 -0
- package/dist/src/lib/run-status.generated.js.map +1 -0
- package/dist/src/lib/run-status.js +19 -0
- package/dist/src/lib/run-status.js.map +1 -0
- package/dist/src/lib/server-text-normalization.d.ts +51 -0
- package/dist/src/lib/server-text-normalization.js +90 -0
- package/dist/src/lib/server-text-normalization.js.map +1 -0
- package/dist/src/lib/server-url.d.ts +22 -0
- package/dist/src/lib/server-url.js +33 -0
- package/dist/src/lib/server-url.js.map +1 -0
- package/dist/src/lib/signing-secret-status.d.ts +81 -0
- package/dist/src/lib/signing-secret-status.js +116 -0
- package/dist/src/lib/signing-secret-status.js.map +1 -0
- package/dist/src/lib/skill-installer.d.ts +71 -0
- package/dist/src/lib/skill-installer.js +441 -0
- package/dist/src/lib/skill-installer.js.map +1 -0
- package/dist/src/lib/snapshot-audit-source.d.ts +45 -0
- package/dist/src/lib/snapshot-audit-source.js +58 -0
- package/dist/src/lib/snapshot-audit-source.js.map +1 -0
- package/dist/src/lib/snapshot-audit-store.d.ts +52 -0
- package/dist/src/lib/snapshot-audit-store.js +196 -0
- package/dist/src/lib/snapshot-audit-store.js.map +1 -0
- package/dist/src/lib/snapshot-audit.d.ts +207 -0
- package/dist/src/lib/snapshot-audit.js +431 -0
- package/dist/src/lib/snapshot-audit.js.map +1 -0
- package/dist/src/lib/snapshot-build-rows.d.ts +60 -0
- package/dist/src/lib/snapshot-build-rows.js +87 -0
- package/dist/src/lib/snapshot-build-rows.js.map +1 -0
- package/dist/src/lib/snapshot-build.d.ts +50 -0
- package/dist/src/lib/snapshot-build.js +111 -0
- package/dist/src/lib/snapshot-build.js.map +1 -0
- package/dist/src/lib/snapshot-manifest-layout.d.ts +61 -0
- package/dist/src/lib/snapshot-manifest-layout.js +70 -0
- package/dist/src/lib/snapshot-manifest-layout.js.map +1 -0
- package/dist/src/lib/snapshots.d.ts +98 -0
- package/dist/src/lib/snapshots.js +294 -0
- package/dist/src/lib/snapshots.js.map +1 -0
- package/dist/src/lib/step-run-table.d.ts +43 -0
- package/dist/src/lib/step-run-table.js +129 -0
- package/dist/src/lib/step-run-table.js.map +1 -0
- package/dist/src/lib/storage-pending-retry.d.ts +26 -0
- package/dist/src/lib/storage-pending-retry.js +42 -0
- package/dist/src/lib/storage-pending-retry.js.map +1 -0
- package/dist/src/lib/swift-codegen/agentGenerator.d.ts +42 -0
- package/dist/src/lib/swift-codegen/agentGenerator.js +118 -0
- package/dist/src/lib/swift-codegen/agentGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/banners.d.ts +24 -0
- package/dist/src/lib/swift-codegen/banners.js +25 -0
- package/dist/src/lib/swift-codegen/banners.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbGenerator.d.ts +113 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js +926 -0
- package/dist/src/lib/swift-codegen/dbGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.d.ts +42 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.js +100 -0
- package/dist/src/lib/swift-codegen/dbSwiftTypes.js.map +1 -0
- package/dist/src/lib/swift-codegen/functionGenerator.d.ts +139 -0
- package/dist/src/lib/swift-codegen/functionGenerator.js +462 -0
- package/dist/src/lib/swift-codegen/functionGenerator.js.map +1 -0
- package/dist/src/lib/swift-codegen/generator.d.ts +100 -0
- package/dist/src/lib/swift-codegen/generator.js +457 -0
- package/dist/src/lib/swift-codegen/generator.js.map +1 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.d.ts +87 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js +661 -0
- package/dist/src/lib/swift-codegen/schemaToSwift.js.map +1 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.d.ts +94 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.js +155 -0
- package/dist/src/lib/swift-codegen/siblingSymbols.js.map +1 -0
- package/dist/src/lib/swift-codegen/swiftNaming.d.ts +85 -0
- package/dist/src/lib/swift-codegen/swiftNaming.js +198 -0
- package/dist/src/lib/swift-codegen/swiftNaming.js.map +1 -0
- package/dist/src/lib/sync-dir-selector.d.ts +21 -0
- package/dist/src/lib/sync-dir-selector.js +30 -0
- package/dist/src/lib/sync-dir-selector.js.map +1 -0
- package/dist/src/lib/sync-paths.d.ts +128 -0
- package/dist/src/lib/sync-paths.js +195 -0
- package/dist/src/lib/sync-paths.js.map +1 -0
- package/dist/src/lib/sync-resource-types.d.ts +563 -0
- package/dist/src/lib/sync-resource-types.js +1073 -0
- package/dist/src/lib/sync-resource-types.js.map +1 -0
- package/dist/src/lib/sync-selectors.d.ts +138 -0
- package/dist/src/lib/sync-selectors.js +289 -0
- package/dist/src/lib/sync-selectors.js.map +1 -0
- package/dist/src/lib/template.d.ts +170 -0
- package/dist/src/lib/template.js +484 -68
- package/dist/src/lib/template.js.map +1 -1
- package/dist/src/lib/test-case-file-names.d.ts +40 -0
- package/dist/src/lib/test-case-file-names.js +91 -0
- package/dist/src/lib/test-case-file-names.js.map +1 -0
- package/dist/src/lib/test-case-keys.d.ts +29 -0
- package/dist/src/lib/test-case-keys.js +55 -0
- package/dist/src/lib/test-case-keys.js.map +1 -0
- package/dist/src/lib/test-case-variables.d.ts +29 -0
- package/dist/src/lib/test-case-variables.js +71 -0
- package/dist/src/lib/test-case-variables.js.map +1 -0
- package/dist/src/lib/token-inject.d.ts +56 -0
- package/dist/src/lib/token-inject.js +204 -0
- package/dist/src/lib/token-inject.js.map +1 -0
- package/dist/src/lib/toml-database-config.d.ts +123 -0
- package/dist/src/lib/toml-database-config.js +544 -0
- package/dist/src/lib/toml-database-config.js.map +1 -0
- package/dist/src/lib/toml-metadata-config.d.ts +151 -0
- package/dist/src/lib/toml-metadata-config.js +476 -0
- package/dist/src/lib/toml-metadata-config.js.map +1 -0
- package/dist/src/lib/toml-native-form.d.ts +46 -0
- package/dist/src/lib/toml-native-form.js +78 -0
- package/dist/src/lib/toml-native-form.js.map +1 -0
- package/dist/src/lib/toml-params-validator.d.ts +129 -0
- package/dist/src/lib/toml-params-validator.js +298 -0
- package/dist/src/lib/toml-params-validator.js.map +1 -0
- package/dist/src/lib/toml-scalar-edit.d.ts +43 -0
- package/dist/src/lib/toml-scalar-edit.js +283 -0
- package/dist/src/lib/toml-scalar-edit.js.map +1 -0
- package/dist/src/lib/user-selector.d.ts +24 -0
- package/dist/src/lib/user-selector.js +33 -0
- package/dist/src/lib/user-selector.js.map +1 -0
- package/dist/src/lib/version-check.d.ts +35 -0
- package/dist/src/lib/version-check.js +241 -0
- package/dist/src/lib/version-check.js.map +1 -0
- package/dist/src/lib/watch.d.ts +121 -0
- package/dist/src/lib/watch.js +169 -0
- package/dist/src/lib/watch.js.map +1 -0
- package/dist/src/lib/web-url.d.ts +40 -0
- package/dist/src/lib/web-url.js +76 -0
- package/dist/src/lib/web-url.js.map +1 -0
- package/dist/src/lib/webhook-deliver.d.ts +209 -0
- package/dist/src/lib/webhook-deliver.js +519 -0
- package/dist/src/lib/webhook-deliver.js.map +1 -0
- package/dist/src/lib/workflow-apply.d.ts +110 -0
- package/dist/src/lib/workflow-apply.js +164 -0
- package/dist/src/lib/workflow-apply.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.d.ts +129 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js +269 -0
- package/dist/src/lib/workflow-codegen/generated-schema-descriptor.js.map +1 -0
- package/dist/src/lib/workflow-codegen/generator.d.ts +96 -0
- package/dist/src/lib/workflow-codegen/generator.js +361 -0
- package/dist/src/lib/workflow-codegen/generator.js.map +1 -0
- package/dist/src/lib/workflow-codegen/invokerIR.d.ts +94 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js +76 -0
- package/dist/src/lib/workflow-codegen/invokerIR.js.map +1 -0
- package/dist/src/lib/workflow-codegen/naming.d.ts +33 -0
- package/dist/src/lib/workflow-codegen/naming.js +81 -0
- package/dist/src/lib/workflow-codegen/naming.js.map +1 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.d.ts +80 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js +303 -0
- package/dist/src/lib/workflow-codegen/schemaToTs.js.map +1 -0
- package/dist/src/lib/workflow-config-apply.d.ts +70 -0
- package/dist/src/lib/workflow-config-apply.js +137 -0
- package/dist/src/lib/workflow-config-apply.js.map +1 -0
- package/dist/src/lib/workflow-config-sidecar.d.ts +63 -0
- package/dist/src/lib/workflow-config-sidecar.js +96 -0
- package/dist/src/lib/workflow-config-sidecar.js.map +1 -0
- package/dist/src/lib/workflow-defaults.d.ts +29 -0
- package/dist/src/lib/workflow-defaults.js +41 -0
- package/dist/src/lib/workflow-defaults.js.map +1 -0
- package/dist/src/lib/workflow-fragments.d.ts +64 -0
- package/dist/src/lib/workflow-fragments.js +342 -0
- package/dist/src/lib/workflow-fragments.js.map +1 -0
- package/dist/src/lib/workflow-include-preserve.d.ts +76 -0
- package/dist/src/lib/workflow-include-preserve.js +286 -0
- package/dist/src/lib/workflow-include-preserve.js.map +1 -0
- package/dist/src/lib/workflow-payload.d.ts +98 -0
- package/dist/src/lib/workflow-payload.js +178 -0
- package/dist/src/lib/workflow-payload.js.map +1 -0
- package/dist/src/lib/workflow-toml-validator.d.ts +211 -0
- package/dist/src/lib/workflow-toml-validator.js +770 -0
- package/dist/src/lib/workflow-toml-validator.js.map +1 -0
- package/dist/src/lib/workflow-usage.d.ts +196 -0
- package/dist/src/lib/workflow-usage.js +310 -0
- package/dist/src/lib/workflow-usage.js.map +1 -0
- package/dist/src/types/index.d.ts +591 -0
- package/dist/src/validators.d.ts +65 -0
- package/dist/src/validators.js +64 -0
- package/dist/src/validators.js.map +1 -0
- package/package.json +34 -9
|
@@ -1,107 +1,851 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import { checkbox, confirm, input, select } from "@inquirer/prompts";
|
|
2
|
+
import { appendFileSync, existsSync, readFileSync, readdirSync, writeFileSync, } from "fs";
|
|
3
|
+
import { basename, join, relative, resolve } from "path";
|
|
4
|
+
import { CURRENT_CONFIG_VERSION, PROJECT_TREE_DIR, PROJECT_CONFIG_FILENAME, PROJECT_CONFIG_DISPLAY_NAME, findProjectRoot, saveProjectConfig, loadProjectConfigFromPath, } from "../lib/project-config.js";
|
|
5
|
+
import { resolveAdoptedEnvironment } from "../lib/init-adopt.js";
|
|
6
|
+
import { CANONICAL_SCHEMA_HEADER, FALLBACK_STARTER_SCHEMA, platformLabel, rootAgentsMarkdown, } from "../lib/init-assets.js";
|
|
7
|
+
import { checkInitGuards, planInit } from "../lib/init-plan.js";
|
|
8
|
+
import { resolveProductionRuntime } from "../lib/init-production-env.js";
|
|
9
|
+
import { checkClientWiring, composeSchemaContent, declaredModelNames, findClientSchema, wireClientToSchema, writeCanonicalSchema, } from "../lib/init-schema.js";
|
|
10
|
+
import { declareSharedProjectConfig } from "../lib/init-xcode.js";
|
|
11
|
+
import { deriveCallbackScheme, linkOptInGuidance, missingUrlTypeGuidance, stampCallbackScheme, universalLinkGuidance, } from "../lib/init-ios-links.js";
|
|
12
|
+
import { appHasBothPlatforms, detectClientPlatform, discoverClientPlatforms, } from "../lib/init-client-platforms.js";
|
|
13
|
+
import { planEmailRedirectUriMerge } from "../lib/init-email-redirect-uris.js";
|
|
14
|
+
import { getExplicitEnvName } from "../lib/env-resolver.js";
|
|
6
15
|
import { ApiClient } from "../lib/api-client.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
16
|
+
import { browserLogin } from "../lib/auth-flow.js";
|
|
17
|
+
import { loadCredentials } from "../lib/config.js";
|
|
18
|
+
import { allowGlobalCredentials } from "../lib/credentials-store.js";
|
|
19
|
+
import { isProjectMode, seedCredentialsForEnvInProject, } from "../lib/credentials-store.js";
|
|
20
|
+
import { getCurrentEnvironment, getProjectConfig } from "../lib/env-resolver.js";
|
|
21
|
+
import { normalizeServerUrl } from "../lib/channel.js";
|
|
22
|
+
import { INIT_CONFIG_FILENAME, loadInitConfig, parseInitConfigToml, resolveSkipInstall, resolveUseExistingAppId, validateInitConfig, } from "../lib/init-config.js";
|
|
23
|
+
import { parsePlatformList, resolvePlatforms, } from "../lib/resolve-platform.js";
|
|
24
|
+
import { resolveInitServer } from "../lib/resolve-init-server.js";
|
|
25
|
+
import { validateServerUrl } from "../lib/server-url.js";
|
|
26
|
+
import { DEFAULT_DEV_PORT, resolveDevPort, validateDevPort, } from "../lib/resolve-init-dev-port.js";
|
|
27
|
+
import { DEFAULT_SERVER_URL, PRODUCTION_SERVER_URL } from "../lib/constants.js";
|
|
28
|
+
import { divider, error, info, keyValue, success, warn, } from "../lib/output.js";
|
|
29
|
+
import { installCommand, PRIMITIVE_CLI_PACKAGE } from "../lib/package-manager.js";
|
|
30
|
+
import { gitInitialCommit, gitInitRepo, installPnpmGlobally, installPrimitiveCli, isDirectoryEmpty, isPnpmInstalled, isPortFree, isPrimitiveCliInstalled, isXcodeInstalled, iosTemplateUrl, vueTemplateUrl, resolveTemplateBranch, prepareTemplate, runPnpmInstall, runSwiftPackageResolve, updateEnvFile, updateEnvPort, configurePrimitiveJson, updateViteConfig, } from "../lib/template.js";
|
|
31
|
+
/**
|
|
32
|
+
* The origin a local dev server is served from. ONE spelling: the app's CORS
|
|
33
|
+
* origins, its `emailRedirectUris` seed and the environment's `webUrl` all
|
|
34
|
+
* name this, and a second spelling would be a 400 on the scaffolded app's
|
|
35
|
+
* first sign-in (#2982).
|
|
36
|
+
*/
|
|
37
|
+
export function localDevOrigin(port) {
|
|
38
|
+
return `http://localhost:${port}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Point an existing environment at the app's web counterpart.
|
|
42
|
+
*
|
|
43
|
+
* Returns false — and writes nothing — when there is no config to write, the
|
|
44
|
+
* environment is not in it, or the file cannot be read: an adopting run does
|
|
45
|
+
* not own the project config, and a seed is never worth failing an init over.
|
|
46
|
+
*/
|
|
47
|
+
export function seedEnvironmentWebUrl(projectRoot, environment, webUrl) {
|
|
48
|
+
const configPath = join(projectRoot, PROJECT_TREE_DIR, PROJECT_CONFIG_FILENAME);
|
|
49
|
+
if (!existsSync(configPath))
|
|
50
|
+
return false;
|
|
51
|
+
let config;
|
|
52
|
+
try {
|
|
53
|
+
config = loadProjectConfigFromPath(configPath);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const entry = config.environments[environment];
|
|
59
|
+
if (!entry)
|
|
60
|
+
return false;
|
|
61
|
+
if (entry.webUrl === webUrl)
|
|
62
|
+
return true;
|
|
63
|
+
entry.webUrl = webUrl;
|
|
64
|
+
saveProjectConfig(configPath, config);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Make sure this app allows the dev sign-in callback, so pointing the
|
|
69
|
+
* environment at the web client cannot break email sign-in (#2982).
|
|
70
|
+
*
|
|
71
|
+
* Returns true when the callback is on the app's `emailRedirectUris` — already
|
|
72
|
+
* there, or merged in here. Returns false when it is not and this run must not
|
|
73
|
+
* put it there: no credentials to ask the server with, an app whose allow-list
|
|
74
|
+
* would be NARROWED by writing one (see `planEmailRedirectUriMerge`), or an API
|
|
75
|
+
* call that failed. A false answer costs nothing but the automatic seed: the
|
|
76
|
+
* caller prints the manual step instead, and the app keeps working code-only.
|
|
77
|
+
*/
|
|
78
|
+
async function devCallbackIsAllowListed(appId, callbackUri, credentials) {
|
|
79
|
+
if (!credentials)
|
|
80
|
+
return false;
|
|
81
|
+
try {
|
|
82
|
+
const apiClient = new ApiClient();
|
|
83
|
+
const settings = await apiClient.getAppSettings(appId);
|
|
84
|
+
const plan = planEmailRedirectUriMerge(settings?.emailRedirectUris, callbackUri);
|
|
85
|
+
if (plan.action === "already-listed")
|
|
86
|
+
return true;
|
|
87
|
+
if (plan.action === "leave-alone") {
|
|
88
|
+
warn(`Not touching this app's sign-in link allow-list: ${plan.reason}.`);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
await apiClient.updateAppSettings(appId, { emailRedirectUris: plan.uris });
|
|
92
|
+
success(`Allow-listed the dev sign-in link: ${callbackUri}`);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
warn(`Could not check this app's sign-in link allow-list: ${err.message}`);
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The .gitignore lines `init` writes for Primitive CLI local state — one line,
|
|
102
|
+
* with no per-file exception (#3153, intent criterion 2).
|
|
103
|
+
*
|
|
104
|
+
* That is the whole point of the move: `primitive/` is committed (the config
|
|
105
|
+
* and every environment's TOML, including the shared `.sync-state.json`
|
|
106
|
+
* baseline) and `.primitive/` is machine-local (credentials, this machine's
|
|
107
|
+
* `primitive env use` selection, pre-pull snapshot backups). Before the move
|
|
108
|
+
* one directory meant both things, so the list needed six entries and a
|
|
109
|
+
* comment explaining which files inside it were committed anyway.
|
|
110
|
+
*/
|
|
111
|
+
export const GITIGNORE_LOCAL_STATE_ENTRIES = [".primitive/"];
|
|
112
|
+
/**
|
|
113
|
+
* Ensures the project root's .gitignore lists the Primitive CLI's local state.
|
|
114
|
+
*
|
|
115
|
+
* Split out of `scaffoldProjectConfig` because a run that ADOPTS an existing
|
|
116
|
+
* project (#2890) must not rewrite its config, but still has to make sure the
|
|
117
|
+
* per-machine state a new client's environment produces cannot be committed.
|
|
118
|
+
*/
|
|
119
|
+
export function ensureGitignoreLocalState(targetDir) {
|
|
120
|
+
// One rule: `.primitive/` is machine-local and never committed, `primitive/`
|
|
121
|
+
// is the committed tree. Narrower `.primitive/...` entries left over from an
|
|
122
|
+
// older init are harmless subsets of this line, so they are left alone — the
|
|
123
|
+
// migration document owns tidying an adopted project's .gitignore.
|
|
124
|
+
const gitignorePath = join(targetDir, ".gitignore");
|
|
125
|
+
const gitignoreEntries = [...GITIGNORE_LOCAL_STATE_ENTRIES];
|
|
126
|
+
let linesToAdd = [];
|
|
127
|
+
if (existsSync(gitignorePath)) {
|
|
128
|
+
const existing = readFileSync(gitignorePath, "utf-8")
|
|
129
|
+
.split(/\r?\n/)
|
|
130
|
+
.map((l) => l.trim());
|
|
131
|
+
for (const entry of gitignoreEntries) {
|
|
132
|
+
if (!existing.includes(entry)) {
|
|
133
|
+
linesToAdd.push(entry);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
linesToAdd = [...gitignoreEntries];
|
|
139
|
+
}
|
|
140
|
+
if (linesToAdd.length > 0) {
|
|
141
|
+
try {
|
|
142
|
+
const prefix = existsSync(gitignorePath) ? "\n# Primitive CLI local state\n" : "# Primitive CLI local state\n";
|
|
143
|
+
appendFileSync(gitignorePath, prefix + linesToAdd.join("\n") + "\n");
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
// Non-fatal
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Writes primitive/config.json into the project directory and ensures the
|
|
152
|
+
* .gitignore entry for local state exists. Safe to call in a directory that
|
|
153
|
+
* already has a config — in that case we add/update the "dev" environment
|
|
154
|
+
* instead of overwriting the file.
|
|
155
|
+
*/
|
|
156
|
+
async function scaffoldProjectConfig(opts) {
|
|
157
|
+
const configPath = join(opts.targetDir, PROJECT_TREE_DIR, PROJECT_CONFIG_FILENAME);
|
|
158
|
+
let config;
|
|
159
|
+
if (existsSync(configPath)) {
|
|
160
|
+
// Respect an existing config: merge in the new dev env if missing.
|
|
161
|
+
try {
|
|
162
|
+
config = loadProjectConfigFromPath(configPath);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Existing file is broken — don't clobber it; just warn and skip.
|
|
166
|
+
warn(`${PROJECT_CONFIG_DISPLAY_NAME} exists but is invalid. Skipping project config scaffold.`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (!config.environments.dev) {
|
|
170
|
+
config.environments.dev = {
|
|
171
|
+
apiUrl: opts.serverUrl,
|
|
172
|
+
appId: opts.appId,
|
|
173
|
+
appName: opts.appName,
|
|
174
|
+
};
|
|
175
|
+
if (!config.defaultEnvironment)
|
|
176
|
+
config.defaultEnvironment = "dev";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
config = {
|
|
181
|
+
version: CURRENT_CONFIG_VERSION,
|
|
182
|
+
defaultEnvironment: "dev",
|
|
183
|
+
environments: {
|
|
184
|
+
dev: {
|
|
185
|
+
apiUrl: opts.serverUrl,
|
|
186
|
+
appId: opts.appId,
|
|
187
|
+
appName: opts.appName,
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
saveProjectConfig(configPath, config);
|
|
193
|
+
ensureGitignoreLocalState(opts.targetDir);
|
|
194
|
+
success(`Wrote ${PROJECT_CONFIG_DISPLAY_NAME} with dev environment`);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Prompts for login when credentials are missing or the server has changed.
|
|
198
|
+
* Returns updated credentials on success, or exits/warns on failure.
|
|
199
|
+
* In non-interactive mode, auto-proceeds with login (no confirmation prompt).
|
|
200
|
+
*/
|
|
201
|
+
async function ensureAuthenticated(credentials, serverUrl, serverChanged, context) {
|
|
202
|
+
if (credentials && !serverChanged)
|
|
203
|
+
return credentials;
|
|
204
|
+
if (serverChanged) {
|
|
205
|
+
info(`Switching to ${serverUrl}. You need to log in.`);
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
info(`You need to log in to ${context.reason}.`);
|
|
209
|
+
}
|
|
210
|
+
if (context.nonInteractive && context.allowSkip) {
|
|
211
|
+
warn(`Skipping ${context.reason}; no authentication is available.`);
|
|
212
|
+
warn(`Make sure the app ID is correct.`);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
let shouldLogin;
|
|
216
|
+
if (context.nonInteractive) {
|
|
217
|
+
// In non-interactive mode, auto-proceed with login
|
|
218
|
+
shouldLogin = true;
|
|
219
|
+
info("Attempting automatic login...");
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
shouldLogin = await confirm({
|
|
223
|
+
message: "Log in now?",
|
|
224
|
+
default: true,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
if (!shouldLogin) {
|
|
228
|
+
if (!context.allowSkip) {
|
|
229
|
+
error(`Cannot ${context.reason} without authentication. Use --use-existing-app-id to skip app creation.`);
|
|
230
|
+
// Authentication happens before template prep, so a bail-out here leaves
|
|
231
|
+
// nothing on disk.
|
|
232
|
+
warn(`No project files were written. Re-run \`primitive init\` once you can authenticate.`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
warn(`Skipping. Make sure the app ID is correct.`);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
const result = await browserLogin(serverUrl);
|
|
239
|
+
if (!result.success) {
|
|
240
|
+
error(`Login failed: ${result.error}`);
|
|
241
|
+
warn(`No project files were written. Re-run \`primitive init\` once you can authenticate.`);
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
success(`Logged in as ${result.credentials.email}`);
|
|
245
|
+
return result.credentials;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Finds the name of the project-config environment (if any) whose `apiUrl`
|
|
249
|
+
* matches `serverUrl`, so the project-mode "not logged in" remedy can name a
|
|
250
|
+
* real, working environment instead of a placeholder. Comparison uses the
|
|
251
|
+
* same canonicalization as `resolveInitServer` (trailing-slash-insensitive)
|
|
252
|
+
* so `https://host/` in `primitive/config.json` still matches.
|
|
253
|
+
*/
|
|
254
|
+
function findEnvironmentForServer(serverUrl) {
|
|
255
|
+
const project = getProjectConfig();
|
|
256
|
+
if (!project)
|
|
257
|
+
return undefined;
|
|
258
|
+
const canonicalTarget = normalizeServerUrl(serverUrl) ?? serverUrl;
|
|
259
|
+
return Object.keys(project.environments).find((name) => {
|
|
260
|
+
const apiUrl = project.environments[name].apiUrl;
|
|
261
|
+
return (normalizeServerUrl(apiUrl) ?? apiUrl) === canonicalTarget;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Loads the init config from --config flag or auto-discovers it in CWD.
|
|
266
|
+
* Returns null if no config file is found/specified.
|
|
267
|
+
* Exits with error if config file is specified but invalid.
|
|
268
|
+
*/
|
|
269
|
+
function loadInitConfigFromOptions(options) {
|
|
270
|
+
let config = null;
|
|
271
|
+
if (options.config) {
|
|
272
|
+
// Explicit config path
|
|
273
|
+
const configPath = resolve(process.cwd(), options.config);
|
|
274
|
+
if (!existsSync(configPath)) {
|
|
275
|
+
error(`Config file not found: ${configPath}`);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const content = readFileSync(configPath, "utf-8");
|
|
280
|
+
config = parseInitConfigToml(content);
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
error(`Failed to parse config file: ${err.message}`);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
// Auto-discover in CWD
|
|
289
|
+
config = loadInitConfig(process.cwd());
|
|
290
|
+
}
|
|
291
|
+
if (!config)
|
|
292
|
+
return null;
|
|
293
|
+
// Validate
|
|
294
|
+
info(`Using config file for non-interactive init`);
|
|
295
|
+
const validation = validateInitConfig(config);
|
|
296
|
+
if (!validation.valid) {
|
|
297
|
+
error("Invalid config file:");
|
|
298
|
+
for (const err of validation.errors) {
|
|
299
|
+
error(` - ${err}`);
|
|
300
|
+
}
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
return config;
|
|
304
|
+
}
|
|
10
305
|
export function registerInitCommand(program) {
|
|
11
306
|
program
|
|
12
307
|
.command("init")
|
|
13
308
|
.description("Create a new Primitive app from template")
|
|
14
309
|
.argument("[directory]", "Directory to create the project in (default: current directory)")
|
|
15
310
|
.option("--use-existing-app-id <id>", "Use an existing app ID instead of creating a new one")
|
|
16
|
-
.option("-s, --server <url>", "Server URL
|
|
17
|
-
.option("--skip-install", "Skip
|
|
311
|
+
.option("-s, --server <url>", "Server URL (default: current CLI server or https://primitiveapi.com)")
|
|
312
|
+
.option("--skip-install", "Skip dependency installation")
|
|
313
|
+
.option("--config <path>", `Path to an init config file for non-interactive setup (default: auto-discover ${INIT_CONFIG_FILENAME} in the current directory)`)
|
|
314
|
+
.option("-p, --platform <platforms>", "Client platform(s) to scaffold: web, ios (SwiftUI), or both as a comma-separated list (web,ios). Omit to be prompted interactively.")
|
|
18
315
|
.addHelpText("after", `
|
|
19
316
|
Examples:
|
|
20
|
-
$ primitive init my-app
|
|
21
|
-
$ primitive init
|
|
317
|
+
$ primitive init my-app # Create web app in ./my-app
|
|
318
|
+
$ primitive init # Interactive: prompt for directory and app name
|
|
319
|
+
$ primitive init my-app --platform ios # Create an iOS (SwiftUI) app
|
|
320
|
+
$ primitive init my-app --platform web,ios # One app, a web client and an iOS client
|
|
321
|
+
$ primitive init ios --platform ios # Add an iOS client to the app of the surrounding project
|
|
22
322
|
$ primitive init . --use-existing-app-id 01HXY... # Use existing app in current dir
|
|
23
323
|
$ primitive init my-app --skip-install # Skip dependency install
|
|
324
|
+
$ primitive init --config .primitive-init.toml # Non-interactive from config file
|
|
325
|
+
|
|
326
|
+
One app, several clients:
|
|
327
|
+
Selecting more than one platform scaffolds them side by side under one project:
|
|
328
|
+
each client in its own directory (web/, ios/), with primitive/config.json, the
|
|
329
|
+
git repository and the shared models/models.toml at the root. Run inside a
|
|
330
|
+
directory that already has an ancestor primitive/config.json and init ADDS the
|
|
331
|
+
client to that app instead: it reuses the selected environment's app, writes no
|
|
332
|
+
nested .primitive/ or .git/, makes no commit, and leaves the files for you to
|
|
333
|
+
review and commit in the outer repository.
|
|
334
|
+
|
|
335
|
+
Non-interactive mode:
|
|
336
|
+
Create a ${INIT_CONFIG_FILENAME} file (or pass --config <path>) to skip all prompts.
|
|
337
|
+
The config file uses TOML format:
|
|
338
|
+
|
|
339
|
+
action = "create" # "create", "use-existing", or "add-client"
|
|
340
|
+
app_name = "My App" # Required for action = "create"
|
|
341
|
+
app_id = "01HXY..." # Required for action = "use-existing"
|
|
342
|
+
dir = "my-app" # Project directory (optional)
|
|
343
|
+
platform = "web" # "web", "ios", "web,ios", or ["web", "ios"]
|
|
344
|
+
# (optional, defaults to web)
|
|
345
|
+
promote_schema = false # Only with action = "add-client". Consent to move
|
|
346
|
+
# an existing single-client repo's models.toml to
|
|
347
|
+
# <project root>/models/models.toml so every client
|
|
348
|
+
# reads one copy.
|
|
349
|
+
access_mode = "invite-only" # "public", "domain", or "invite-only" (optional)
|
|
350
|
+
dev_port = 5173 # Dev server port (optional). Set it to skip the
|
|
351
|
+
# port prompt: init pins the port in
|
|
352
|
+
# vite.config.ts and .env and adds
|
|
353
|
+
# http://localhost:<port> to the app's CORS
|
|
354
|
+
# allowed origins and redirect URIs.
|
|
355
|
+
skip_install = false # Skip pnpm install (optional). Same effect as
|
|
356
|
+
# --skip-install; the flag wins if both are set.
|
|
357
|
+
server = "https://..." # Server to create the app on (optional).
|
|
358
|
+
# Same effect as -s/--server; the flag wins if
|
|
359
|
+
# both are set. If the CLI already points at a
|
|
360
|
+
# different server, init stops and tells you how
|
|
361
|
+
# to point it here first. Outside a Primitive
|
|
362
|
+
# project, if you are not logged in at all, init
|
|
363
|
+
# logs in to this one; inside a project the
|
|
364
|
+
# selected environment's apiUrl is the target
|
|
365
|
+
# even when logged out, so a different value
|
|
366
|
+
# here stops the run.
|
|
367
|
+
invite_emails = ["a@b.com"] # Emails to invite (optional, for invite-only)
|
|
368
|
+
allowed_domains = ["b.com"] # Allowed domains (optional, for domain mode)
|
|
369
|
+
overwrite = false # Overwrite non-empty target directory (optional)
|
|
24
370
|
|
|
25
371
|
This command will:
|
|
26
|
-
1. Download
|
|
27
|
-
2. Prompt for an app name and create it on the Primitive server (or use existing
|
|
28
|
-
|
|
29
|
-
|
|
372
|
+
1. Download a Primitive app template for each selected platform (web, Apple SwiftUI)
|
|
373
|
+
2. Prompt for an app name and create it on the Primitive server (or use existing,
|
|
374
|
+
or adopt the app of the project it is run inside)
|
|
375
|
+
3. Configure the project with your app ID and point every client at one model schema
|
|
376
|
+
4. Install dependencies (pnpm for web, swift package resolve for Apple)
|
|
30
377
|
`)
|
|
31
378
|
.action(handleInit);
|
|
32
379
|
}
|
|
33
380
|
async function handleInit(projectDirArg, options) {
|
|
34
|
-
|
|
381
|
+
// `init` is what CREATES a project, so by definition it runs before one
|
|
382
|
+
// exists — and it has to call the API to do it. One of the four commands
|
|
383
|
+
// permitted to reach the global credentials file with no project in scope
|
|
384
|
+
// (#3154, intent criterion 5).
|
|
385
|
+
allowGlobalCredentials();
|
|
386
|
+
// Validate an explicitly-passed `--platform` flag up front. Only `web` and
|
|
387
|
+
// `ios` are supported (#1009 removed `macos`), and the flag names a LIST —
|
|
388
|
+
// `--platform web,ios` scaffolds both clients of one app (#2890). An omitted
|
|
389
|
+
// flag is `undefined` (the commander default was dropped) and is resolved
|
|
390
|
+
// below.
|
|
391
|
+
let flagPlatforms;
|
|
392
|
+
if (options.platform !== undefined) {
|
|
393
|
+
const parsed = parsePlatformList(options.platform);
|
|
394
|
+
if (!parsed.ok) {
|
|
395
|
+
error(parsed.error);
|
|
396
|
+
process.exit(1);
|
|
397
|
+
}
|
|
398
|
+
flagPlatforms = parsed.platforms;
|
|
399
|
+
}
|
|
400
|
+
// `initConfig` is the parsed-and-validated `primitive-init.toml` for
|
|
401
|
+
// non-interactive runs (CI, scripted scaffolding). When non-null, the
|
|
402
|
+
// rest of this function uses it to skip prompts.
|
|
403
|
+
const initConfig = loadInitConfigFromOptions(options);
|
|
404
|
+
// Whether to skip the dependency install (steps 7-8). Both the
|
|
405
|
+
// `--skip-install` flag and the config's `skip_install` key request it; the
|
|
406
|
+
// flag wins. Reading only the flag is what made the documented config key a
|
|
407
|
+
// no-op (#2720).
|
|
408
|
+
const { skipInstall, source: skipInstallSource } = resolveSkipInstall(options.skipInstall, initConfig);
|
|
409
|
+
const skipInstallReason = skipInstallSource === "config" ? "skip_install = true in config" : "--skip-install";
|
|
410
|
+
// Resolve the target platform per the 4-case interactivity contract:
|
|
411
|
+
// (a) explicit --platform flag -> use it (never prompt)
|
|
412
|
+
// (b) init-config TOML present -> use its platform or fall back to web
|
|
413
|
+
// (c) interactive TTY, no flag/cfg -> prompt
|
|
414
|
+
// (d) non-TTY, no flag/cfg -> fall back to web (no hang/crash)
|
|
415
|
+
// The prompt is TTY-guarded inside `resolvePlatform` so a piped/CI stdin
|
|
416
|
+
// never triggers the `@inquirer/prompts` `ERR_USE_AFTER_CLOSE` crash.
|
|
417
|
+
// The init config's `platform` was validated above, so it parses.
|
|
418
|
+
const configPlatforms = initConfig?.platform !== undefined
|
|
419
|
+
? parsePlatformList(initConfig.platform).platforms
|
|
420
|
+
: undefined;
|
|
421
|
+
const platforms = await resolvePlatforms({
|
|
422
|
+
flag: flagPlatforms,
|
|
423
|
+
configPlatform: configPlatforms,
|
|
424
|
+
hasConfig: initConfig != null,
|
|
425
|
+
isTTY: Boolean(process.stdin.isTTY),
|
|
426
|
+
promptFn: () => checkbox({
|
|
427
|
+
message: "Which clients are you building? (space to select)",
|
|
428
|
+
choices: [
|
|
429
|
+
{ value: "web", name: "web (Vue 3 + TypeScript + Tailwind)", checked: true },
|
|
430
|
+
{ value: "ios", name: "ios (Swift + SwiftUI)" },
|
|
431
|
+
],
|
|
432
|
+
// A run with nothing selected has nothing to scaffold; re-ask rather
|
|
433
|
+
// than proceeding with an empty list.
|
|
434
|
+
validate: (choices) => choices.length > 0 ? true : "Select at least one client.",
|
|
435
|
+
}),
|
|
436
|
+
});
|
|
437
|
+
// iOS brings the SwiftUI template, the Xcode toolchain and the
|
|
438
|
+
// primitive.json config path — whether it is the only client or one of
|
|
439
|
+
// several. (macOS was removed in #1009.)
|
|
440
|
+
const isApple = platforms.includes("ios");
|
|
441
|
+
// Apple-family platforms require macOS with Xcode.
|
|
442
|
+
if (isApple && process.platform !== "darwin") {
|
|
443
|
+
error("iOS apps require macOS with Xcode installed.");
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
if (isApple) {
|
|
447
|
+
const xcodeAvailable = await isXcodeInstalled();
|
|
448
|
+
if (!xcodeAvailable) {
|
|
449
|
+
error("Xcode command-line tools are required for Apple development. Install them with: xcode-select --install");
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
35
453
|
// Step 1: Determine project directory
|
|
454
|
+
//
|
|
455
|
+
// Run from inside an existing Primitive app, one platform goes in a
|
|
456
|
+
// directory named after itself (`ios/`, `web/`) rather than in a new
|
|
457
|
+
// project's. That default is also what makes an `add-client` init config
|
|
458
|
+
// non-interactive without a `dir` key (#2890). A run selecting several
|
|
459
|
+
// platforms names the directory their subdirectories go under, which has no
|
|
460
|
+
// obvious default, so it is asked for as before.
|
|
36
461
|
let projectDir = projectDirArg;
|
|
462
|
+
if (!projectDir && initConfig?.dir) {
|
|
463
|
+
projectDir = initConfig.dir;
|
|
464
|
+
}
|
|
465
|
+
const clientDirDefault = platforms.length === 1 && findProjectRoot(process.cwd()) ? platforms[0] : null;
|
|
466
|
+
if (!projectDir && clientDirDefault && initConfig) {
|
|
467
|
+
projectDir = clientDirDefault;
|
|
468
|
+
}
|
|
37
469
|
if (!projectDir) {
|
|
38
470
|
projectDir = await input({
|
|
39
|
-
message:
|
|
40
|
-
|
|
471
|
+
message: clientDirDefault
|
|
472
|
+
? "Directory for the new client:"
|
|
473
|
+
: "Project name (a new directory will be created):",
|
|
474
|
+
default: clientDirDefault ?? "my-primitive-app",
|
|
41
475
|
validate: (value) => {
|
|
42
476
|
if (!value.trim())
|
|
43
|
-
return "
|
|
477
|
+
return "Project name cannot be empty";
|
|
44
478
|
return true;
|
|
45
479
|
},
|
|
46
480
|
});
|
|
47
481
|
}
|
|
48
482
|
const targetDir = resolve(process.cwd(), projectDir);
|
|
49
483
|
// Used as the default app name suggestion when prompting
|
|
50
|
-
const defaultAppName = projectDir === "." ? basename(targetDir) : projectDir;
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
484
|
+
const defaultAppName = projectDir === "." ? basename(targetDir) : basename(projectDir);
|
|
485
|
+
// ── Which layout is this run producing? (#2890) ────────────────────────
|
|
486
|
+
//
|
|
487
|
+
// A `primitive/config.json` in an ANCESTOR of the target means this app
|
|
488
|
+
// already exists and this run adds a client to it. Passing the ancestor only
|
|
489
|
+
// when it differs from the target keeps `primitive init .` at a project root
|
|
490
|
+
// on its existing merge behavior.
|
|
491
|
+
const ancestorRoot = findProjectRoot(targetDir);
|
|
492
|
+
const adoptedRoot = ancestorRoot && resolve(ancestorRoot) !== resolve(targetDir) ? ancestorRoot : null;
|
|
493
|
+
const plan = planInit({
|
|
494
|
+
platforms,
|
|
495
|
+
targetDir,
|
|
496
|
+
ancestorProjectRoot: adoptedRoot,
|
|
497
|
+
});
|
|
498
|
+
// The adopted app's identity comes from the project config's selected
|
|
499
|
+
// environment — never from a flag or a prompt.
|
|
500
|
+
let adopted = null;
|
|
501
|
+
if (adoptedRoot) {
|
|
502
|
+
try {
|
|
503
|
+
adopted = resolveAdoptedEnvironment(adoptedRoot, {
|
|
504
|
+
explicitEnvName: getExplicitEnvName(),
|
|
505
|
+
envVarName: process.env.PRIMITIVE_ENV || null,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
error(`Could not read the Primitive project at ${adoptedRoot}: ${err.message}`);
|
|
510
|
+
warn("No project files were written.");
|
|
60
511
|
process.exit(1);
|
|
61
512
|
}
|
|
62
513
|
}
|
|
63
|
-
// Step
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
514
|
+
// Step 2: Check if the target — and each client directory under it — exists
|
|
515
|
+
// and is non-empty. Each directory follows the same overwrite contract.
|
|
516
|
+
const directoriesToGuard = [targetDir, ...plan.clients.map((c) => c.dir)].filter((dir, index, all) => all.indexOf(dir) === index);
|
|
517
|
+
for (const dir of directoriesToGuard) {
|
|
518
|
+
if (!existsSync(dir) || isDirectoryEmpty(dir))
|
|
519
|
+
continue;
|
|
520
|
+
const label = dir === targetDir ? projectDir : relative(process.cwd(), dir);
|
|
521
|
+
if (initConfig) {
|
|
522
|
+
// Non-interactive: require explicit overwrite opt-in
|
|
523
|
+
if (!initConfig.overwrite) {
|
|
524
|
+
error(`Directory "${label}" already exists and is not empty.`);
|
|
525
|
+
error('Set overwrite = true in your config file to allow overwriting.');
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
warn(`Directory "${label}" already exists and is not empty. Overwriting (overwrite = true in config).`);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
warn(`Directory "${label}" already exists and is not empty.`);
|
|
532
|
+
const overwrite = await confirm({
|
|
533
|
+
message: "Do you want to overwrite the existing contents?",
|
|
534
|
+
default: false,
|
|
535
|
+
});
|
|
536
|
+
if (!overwrite) {
|
|
537
|
+
error("Aborted. Please choose a different project name.");
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
70
541
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
542
|
+
// Step 2.5: Determine target server. Precedence: `--server` flag, then the
|
|
543
|
+
// init-config `server` key, then the current login, then the default. The
|
|
544
|
+
// config key used to be dropped on the floor here (#2376).
|
|
545
|
+
if (options.server !== undefined) {
|
|
546
|
+
const validation = validateServerUrl(options.server);
|
|
547
|
+
if (validation !== true) {
|
|
548
|
+
error(validation);
|
|
549
|
+
process.exit(1);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
const existingCredentials = loadCredentials();
|
|
553
|
+
// In project mode the CLI is pinned to the selected environment's `apiUrl`
|
|
554
|
+
// even before that environment has any credentials: `saveCredentialsStore()`
|
|
555
|
+
// writes tokens into the environment's slot and discards the server they were
|
|
556
|
+
// minted for, and `loadCredentialsStore()` blends them back with
|
|
557
|
+
// `env.config.apiUrl`. So "logged out" does not mean "no current server"
|
|
558
|
+
// there — the environment still fixes the target, and the mismatch check
|
|
559
|
+
// below has to compare against it. Reading only the credentials' `serverUrl`
|
|
560
|
+
// let a logged-out environment slip past the guard: init would log in to the
|
|
561
|
+
// config's server and then send every later call to the environment's own
|
|
562
|
+
// `apiUrl`.
|
|
563
|
+
const currentEnvironment = getCurrentEnvironment();
|
|
564
|
+
// When adopting, the app already lives somewhere: the environment being
|
|
565
|
+
// adopted fixes the server, and a `--server` that disagrees is a mistake
|
|
566
|
+
// rather than a choice.
|
|
567
|
+
const currentServer = adopted
|
|
568
|
+
? adopted.config.apiUrl
|
|
569
|
+
: existingCredentials?.serverUrl ?? currentEnvironment?.config.apiUrl;
|
|
570
|
+
const resolvedServer = await resolveInitServer({
|
|
571
|
+
flagServer: options.server,
|
|
572
|
+
configServer: initConfig?.server,
|
|
573
|
+
hasConfig: initConfig != null,
|
|
574
|
+
currentServer,
|
|
575
|
+
defaultServer: DEFAULT_SERVER_URL,
|
|
576
|
+
// An adopt run has nowhere else to put the client: the environment it is
|
|
577
|
+
// adopting names the server, so there is nothing to ask about.
|
|
578
|
+
isTTY: Boolean(process.stdin.isTTY) && plan.mode !== "add-client",
|
|
579
|
+
promptFn: async () => {
|
|
580
|
+
// Only reached when a current server exists (see resolveInitServer).
|
|
581
|
+
const current = currentServer ?? DEFAULT_SERVER_URL;
|
|
582
|
+
console.log();
|
|
583
|
+
const serverChoice = await select({
|
|
584
|
+
message: `The Primitive CLI is currently using ${current}.\nWhere do you want to create this app?`,
|
|
585
|
+
choices: [
|
|
586
|
+
{
|
|
587
|
+
value: current,
|
|
588
|
+
name: `Current server (${current})`,
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
value: DEFAULT_SERVER_URL,
|
|
592
|
+
name: `Production (${DEFAULT_SERVER_URL})`,
|
|
593
|
+
},
|
|
594
|
+
{
|
|
595
|
+
value: "__custom__",
|
|
596
|
+
name: "Other server",
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
default: currentServer,
|
|
600
|
+
});
|
|
601
|
+
if (serverChoice !== "__custom__")
|
|
602
|
+
return serverChoice;
|
|
603
|
+
return input({
|
|
604
|
+
message: "Enter the server URL:",
|
|
605
|
+
validate: validateServerUrl,
|
|
606
|
+
});
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
const serverUrl = resolvedServer.serverUrl;
|
|
610
|
+
const serverChanged = resolvedServer.serverChanged;
|
|
611
|
+
if (resolvedServer.source === "config") {
|
|
612
|
+
info(`Server: ${serverUrl} (from config)`);
|
|
613
|
+
}
|
|
614
|
+
// A run that can't get usable credentials for the resolved server must fail
|
|
615
|
+
// loudly here rather than hang on the OAuth callback — or, as in #2376,
|
|
616
|
+
// quietly build the app on the current server. Two cases qualify:
|
|
617
|
+
//
|
|
618
|
+
// * a non-interactive init-config run, which has no way to drive a browser;
|
|
619
|
+
// * any project-mode run, because a login always lands in the selected
|
|
620
|
+
// environment's slot and is read back with that environment's `apiUrl`.
|
|
621
|
+
// Logging in to a different server there would leave the project holding
|
|
622
|
+
// credentials the rest of the CLI sends somewhere else.
|
|
623
|
+
//
|
|
624
|
+
// In legacy mode an interactive run can still log in to the requested server
|
|
625
|
+
// and have it stored correctly, so it goes through the login flow below.
|
|
626
|
+
const inProjectMode = isProjectMode() || plan.mode === "add-client";
|
|
627
|
+
if (serverChanged && (initConfig || inProjectMode)) {
|
|
628
|
+
const requestedBy = resolvedServer.source === "flag"
|
|
629
|
+
? "--server"
|
|
630
|
+
: resolvedServer.source === "config"
|
|
631
|
+
? "the init config's `server`"
|
|
632
|
+
: "the server you selected";
|
|
633
|
+
// `currentServer` is the stored login's server when there is one, and
|
|
634
|
+
// otherwise the selected environment's `apiUrl` — say which, so the user
|
|
635
|
+
// isn't told they're "logged in to" a server they never logged in to.
|
|
636
|
+
const currentDescription = existingCredentials
|
|
637
|
+
? `the CLI is logged in to ${currentServer}`
|
|
638
|
+
: `this project's "${adopted?.name ?? currentEnvironment?.name}" environment targets ${currentServer}`;
|
|
639
|
+
error(`Not logged in to ${serverUrl}.`);
|
|
640
|
+
error(`${requestedBy} requests ${serverUrl}, but ${currentDescription}.`);
|
|
641
|
+
// The remedy differs by mode: `primitive login` resolves its target
|
|
642
|
+
// through `getConfiguredServerUrl()`, which returns the active
|
|
643
|
+
// environment's `apiUrl` whenever a project config is in scope and ignores
|
|
644
|
+
// `PRIMITIVE_SERVER_URL` there. Printing the legacy-mode hint inside a
|
|
645
|
+
// project would send the user in circles.
|
|
646
|
+
if (inProjectMode) {
|
|
647
|
+
error(`Inside a Primitive project, \`primitive login\` targets the selected environment's server, so PRIMITIVE_SERVER_URL has no effect here.`);
|
|
648
|
+
// `primitive -e <env> login` only resolves the mismatch if `<env>` is
|
|
649
|
+
// both logged into AND passed to the init re-run — `currentServer` is
|
|
650
|
+
// always the *selected* environment's apiUrl (that's what made this
|
|
651
|
+
// serverChanged in the first place), so re-running `primitive login`
|
|
652
|
+
// or `primitive init` without `-e` reproduces the identical error.
|
|
653
|
+
// Look up which configured environment (if any) actually targets
|
|
654
|
+
// `serverUrl` so the hint names a real, working command instead of a
|
|
655
|
+
// placeholder.
|
|
656
|
+
const targetEnvName = findEnvironmentForServer(serverUrl);
|
|
657
|
+
// The re-run has to resolve to the same server, and it re-reads every
|
|
658
|
+
// input from scratch. Only a config-selected server survives on its own:
|
|
659
|
+
// when the flag (or the interactive prompt) is what won here, an
|
|
660
|
+
// otherwise-identical re-run would fall back to the config's `server` —
|
|
661
|
+
// or prompt again — and reproduce this exact error. Pin it with
|
|
662
|
+
// `--server`, which outranks both.
|
|
663
|
+
const serverFlag = resolvedServer.source === "config" ? "" : ` --server ${serverUrl}`;
|
|
664
|
+
if (targetEnvName) {
|
|
665
|
+
const configFlag = options.config ? ` --config ${options.config}` : "";
|
|
666
|
+
error(`Log in to the "${targetEnvName}" environment and run init against it: ` +
|
|
667
|
+
`\`primitive -e ${targetEnvName} login\` then ` +
|
|
668
|
+
`\`primitive -e ${targetEnvName} init${configFlag}${serverFlag}\`.`);
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
error(`This project has no environment configured for ${serverUrl}. ` +
|
|
672
|
+
`Add one (\`primitive env add <name> --api-url ${serverUrl} --app-id <app-id>\`) and ` +
|
|
673
|
+
`re-run with \`-e <name>${serverFlag}\`, or re-run init outside the project directory.`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
error(`Log in to the target server first: PRIMITIVE_SERVER_URL=${serverUrl} primitive login`);
|
|
678
|
+
}
|
|
679
|
+
warn("No project files were written.");
|
|
680
|
+
process.exit(1);
|
|
681
|
+
}
|
|
682
|
+
// Everything this run must be refused for, before a download or a write.
|
|
683
|
+
const guardError = checkInitGuards({
|
|
684
|
+
action: initConfig?.action,
|
|
685
|
+
hasConfig: initConfig != null,
|
|
686
|
+
ancestorProjectRoot: adoptedRoot,
|
|
687
|
+
configAppId: initConfig?.app_id,
|
|
688
|
+
flagAppId: options.useExistingAppId,
|
|
689
|
+
environmentAppId: adopted?.config.appId,
|
|
690
|
+
environmentName: adopted?.name,
|
|
691
|
+
});
|
|
692
|
+
if (guardError) {
|
|
693
|
+
error(guardError);
|
|
694
|
+
warn("No project files were written.");
|
|
74
695
|
process.exit(1);
|
|
75
696
|
}
|
|
76
|
-
|
|
77
|
-
|
|
697
|
+
if (plan.mode === "add-client") {
|
|
698
|
+
info(`Found an existing Primitive project at ${adoptedRoot} (environment "${adopted.name}").`);
|
|
699
|
+
// A run driven by a config already said what it wants; anything else asks,
|
|
700
|
+
// and a piped stdin gets the key that says it non-interactively rather than
|
|
701
|
+
// `confirmPrompt`'s `--yes` remedy, which init has no flag for.
|
|
702
|
+
if (!initConfig) {
|
|
703
|
+
if (!process.stdin.isTTY) {
|
|
704
|
+
error(`Adding a client to the project at ${adoptedRoot} needs a confirmation, and stdin is not interactive.`);
|
|
705
|
+
error(`Re-run with an init config containing \`action = "add-client"\` to confirm non-interactively.`);
|
|
706
|
+
warn("No project files were written.");
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
const proceed = await confirm({
|
|
710
|
+
message: `Add this client to app ${adopted.config.appName ?? adopted.config.appId} in ${adoptedRoot}?`,
|
|
711
|
+
default: true,
|
|
712
|
+
});
|
|
713
|
+
if (!proceed) {
|
|
714
|
+
error("Aborted. No project files were written.");
|
|
715
|
+
process.exit(1);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// ── The one schema (#2890) ─────────────────────────────────────────────
|
|
720
|
+
//
|
|
721
|
+
// Decided here, before anything is downloaded or written, because the answer
|
|
722
|
+
// can be "stop": an adopted repository whose client keeps its schema
|
|
723
|
+
// somewhere this run does not recognize, or one whose owner declines the
|
|
724
|
+
// move, must end with nothing changed.
|
|
725
|
+
const schemaPlan = plan.canonicalSchemaPath
|
|
726
|
+
? await resolveSchemaComposition({
|
|
727
|
+
plan,
|
|
728
|
+
promoteConsent: initConfig?.promote_schema === true,
|
|
729
|
+
interactive: initConfig == null && Boolean(process.stdin.isTTY),
|
|
730
|
+
nonInteractive: initConfig != null,
|
|
731
|
+
})
|
|
732
|
+
: null;
|
|
733
|
+
// Step 4: Check authentication (credentials were loaded for the server
|
|
734
|
+
// resolution above — reuse them rather than re-reading the file).
|
|
735
|
+
let credentials = existingCredentials;
|
|
78
736
|
let appId;
|
|
79
737
|
let appName;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
738
|
+
/**
|
|
739
|
+
* Whether THIS run created the app. A created app is seeded with
|
|
740
|
+
* `http://localhost:5173/oauth/callback` in `emailRedirectUris`; an adopted
|
|
741
|
+
* one carries whatever allow-list it already had, which is why the combined
|
|
742
|
+
* app's `webUrl` seed asks the server before pointing the sign-in link at a
|
|
743
|
+
* dev callback (#2982).
|
|
744
|
+
*/
|
|
745
|
+
let appWasCreatedByThisRun = false;
|
|
746
|
+
const preparedTemplates = new Map();
|
|
747
|
+
const cleanupPreparedTemplate = () => {
|
|
748
|
+
for (const prepared of preparedTemplates.values()) {
|
|
749
|
+
try {
|
|
750
|
+
prepared.cleanup();
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
// Non-fatal cleanup best effort
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
preparedTemplates.clear();
|
|
757
|
+
};
|
|
758
|
+
const prepareTemplateOrExit = async () => {
|
|
759
|
+
// EVERY selected template downloads and validates before the app is
|
|
760
|
+
// created, and none of them touches the target directory yet. That keeps
|
|
761
|
+
// both failure modes covered with several clients in play: app-creation
|
|
762
|
+
// failure leaves no scaffold, and a download failure — of either template —
|
|
763
|
+
// leaves no orphaned app and nothing half-written (#1218, #2890).
|
|
764
|
+
for (const client of plan.clients) {
|
|
765
|
+
if (preparedTemplates.has(client.platform))
|
|
766
|
+
continue;
|
|
767
|
+
info(`Preparing your ${platformLabel(client.platform)} project template...`);
|
|
768
|
+
// The branch is the channel (issue #1934): scaffolding against the alpha
|
|
769
|
+
// server pulls the alpha template branch, whose Primitive deps pin the
|
|
770
|
+
// alpha prerelease libraries. Falls back to main (with a warning) if the
|
|
771
|
+
// channel branch isn't available.
|
|
772
|
+
const templateBranch = resolveTemplateBranch(serverUrl);
|
|
773
|
+
const templateUrlFor = client.platform === "ios" ? iosTemplateUrl : vueTemplateUrl;
|
|
774
|
+
try {
|
|
775
|
+
preparedTemplates.set(client.platform, await prepareTemplate({
|
|
776
|
+
templateUrl: templateUrlFor(templateBranch),
|
|
777
|
+
fallbackTemplateUrl: templateBranch !== "main" ? templateUrlFor("main") : undefined,
|
|
778
|
+
onFallback: (reason) => warn(`Template branch "${templateBranch}" unavailable (${reason}) — falling back to main.`),
|
|
779
|
+
onProgress: info,
|
|
780
|
+
}));
|
|
781
|
+
}
|
|
782
|
+
catch (err) {
|
|
783
|
+
error(`Failed to download template: ${err.message}`);
|
|
784
|
+
error("Check your internet connection and try again.");
|
|
785
|
+
// A template that already downloaded must not survive as a temp
|
|
786
|
+
// directory when the run stops here.
|
|
787
|
+
cleanupPreparedTemplate();
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
// An existing app can be selected two ways: the `--use-existing-app-id` flag,
|
|
793
|
+
// or `action = "use-existing"` + `app_id` in the init-config TOML. Reading
|
|
794
|
+
// only the flag (the old behavior) made a use-existing TOML fall through to
|
|
795
|
+
// the create branch and prompt for an app name — hanging non-interactive
|
|
796
|
+
// runs. (#1217)
|
|
797
|
+
const useExistingAppId = resolveUseExistingAppId(options.useExistingAppId, initConfig);
|
|
798
|
+
if (plan.mode === "add-client") {
|
|
799
|
+
// ── Adopt: the app is the one this project already targets ──────────
|
|
800
|
+
//
|
|
801
|
+
// `appName` is OPTIONAL in a valid environment, so it is resolved rather
|
|
802
|
+
// than assumed: the environment's name, else the app's name from the API
|
|
803
|
+
// when we are authenticated, else the client directory's own name. No
|
|
804
|
+
// downstream writer ever receives an undefined display name, and no
|
|
805
|
+
// scaffold ships an unresolved placeholder.
|
|
806
|
+
appId = adopted.config.appId;
|
|
807
|
+
appName = adopted.config.appName ?? "";
|
|
808
|
+
// Credentials are resolved from the working directory, while the app comes
|
|
809
|
+
// from the project the TARGET sits in — a target inside a different project
|
|
810
|
+
// (or a run from outside any) can leave the CLI holding another server's
|
|
811
|
+
// token, and every API call here would go there with this app's id. The
|
|
812
|
+
// run continues unauthenticated instead: nothing it writes needs the API.
|
|
813
|
+
const credentialServer = credentials ? normalizeServerUrl(credentials.serverUrl) : null;
|
|
814
|
+
if (credentialServer && credentialServer !== normalizeServerUrl(adopted.config.apiUrl)) {
|
|
815
|
+
warn(`The CLI is logged in to ${credentials.serverUrl}, but the "${adopted.name}" environment in ${adoptedRoot} targets ${adopted.config.apiUrl}.`);
|
|
816
|
+
warn(`Continuing without contacting either server. Run init from inside that project, or log in to ${adopted.config.apiUrl}, to have the app verified.`);
|
|
817
|
+
credentials = null;
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
credentials = await ensureAuthenticated(credentials, serverUrl, serverChanged, {
|
|
821
|
+
reason: "verify the app",
|
|
822
|
+
allowSkip: true,
|
|
823
|
+
nonInteractive: !!initConfig,
|
|
89
824
|
});
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
warn("You'll need to manually set VITE_APP_ID in your .env file to use this project.");
|
|
96
|
-
process.exit(1);
|
|
97
|
-
}
|
|
98
|
-
credentials = result.credentials;
|
|
99
|
-
success(`Logged in as ${credentials.email}`);
|
|
825
|
+
}
|
|
826
|
+
if (!appName && credentials) {
|
|
827
|
+
try {
|
|
828
|
+
const client = new ApiClient();
|
|
829
|
+
appName = (await client.getApp(appId)).name;
|
|
100
830
|
}
|
|
101
|
-
|
|
102
|
-
warn(
|
|
831
|
+
catch (err) {
|
|
832
|
+
warn(`Could not look up the app's name: ${err.message}`);
|
|
103
833
|
}
|
|
104
834
|
}
|
|
835
|
+
if (!appName)
|
|
836
|
+
appName = basename(plan.clients[0].dir);
|
|
837
|
+
info(`Adding a ${plan.clients.map((c) => platformLabel(c.platform)).join(" and ")} client to ${appName} (${appId}).`);
|
|
838
|
+
await prepareTemplateOrExit();
|
|
839
|
+
}
|
|
840
|
+
else if (useExistingAppId) {
|
|
841
|
+
// Using existing app ID - verify it exists
|
|
842
|
+
appId = useExistingAppId;
|
|
843
|
+
appName = "(existing app)"; // Default if we can't verify
|
|
844
|
+
credentials = await ensureAuthenticated(credentials, serverUrl, serverChanged, {
|
|
845
|
+
reason: "verify the app",
|
|
846
|
+
allowSkip: true,
|
|
847
|
+
nonInteractive: !!initConfig,
|
|
848
|
+
});
|
|
105
849
|
if (credentials) {
|
|
106
850
|
try {
|
|
107
851
|
const client = new ApiClient();
|
|
@@ -113,36 +857,27 @@ async function handleInit(projectDirArg, options) {
|
|
|
113
857
|
warn(`Could not verify app: ${err.message}`);
|
|
114
858
|
}
|
|
115
859
|
}
|
|
860
|
+
await prepareTemplateOrExit();
|
|
116
861
|
}
|
|
117
862
|
else {
|
|
118
863
|
// Creating new app - authentication required
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
864
|
+
credentials = await ensureAuthenticated(credentials, serverUrl, serverChanged, {
|
|
865
|
+
reason: "create an app",
|
|
866
|
+
allowSkip: false,
|
|
867
|
+
nonInteractive: !!initConfig,
|
|
868
|
+
});
|
|
869
|
+
// Determine app name
|
|
870
|
+
if (initConfig?.app_name) {
|
|
871
|
+
appName = initConfig.app_name;
|
|
872
|
+
info(`App name: ${appName} (from config)`);
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
appName = await input({
|
|
876
|
+
message: "What would you like to name your app?",
|
|
877
|
+
default: defaultAppName,
|
|
124
878
|
});
|
|
125
|
-
if (!shouldLogin) {
|
|
126
|
-
error("Cannot create app without authentication. Use --use-existing-app-id to skip app creation.");
|
|
127
|
-
warn(`Template files were extracted to "${targetDir}" but VITE_APP_ID is not configured.`);
|
|
128
|
-
warn("You'll need to manually set VITE_APP_ID in your .env file to use this project.");
|
|
129
|
-
process.exit(1);
|
|
130
|
-
}
|
|
131
|
-
const result = await browserLogin(serverUrl);
|
|
132
|
-
if (!result.success) {
|
|
133
|
-
error(`Login failed: ${result.error}`);
|
|
134
|
-
warn(`Template files were extracted to "${targetDir}" but VITE_APP_ID is not configured.`);
|
|
135
|
-
warn("You'll need to manually set VITE_APP_ID in your .env file to use this project.");
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}
|
|
138
|
-
credentials = result.credentials;
|
|
139
|
-
success(`Logged in as ${credentials.email}`);
|
|
140
879
|
}
|
|
141
|
-
|
|
142
|
-
appName = await input({
|
|
143
|
-
message: "What would you like to name your app?",
|
|
144
|
-
default: defaultAppName,
|
|
145
|
-
});
|
|
880
|
+
await prepareTemplateOrExit();
|
|
146
881
|
// Create app
|
|
147
882
|
info("Creating app on Primitive server...");
|
|
148
883
|
try {
|
|
@@ -156,58 +891,75 @@ async function handleInit(projectDirArg, options) {
|
|
|
156
891
|
throw new Error("App created but no appId returned");
|
|
157
892
|
}
|
|
158
893
|
appId = id;
|
|
894
|
+
appWasCreatedByThisRun = true;
|
|
159
895
|
success(`App created: ${appName}`);
|
|
160
896
|
keyValue("App ID", appId);
|
|
161
897
|
}
|
|
162
898
|
catch (err) {
|
|
163
899
|
error(`Failed to create app: ${err.message}`);
|
|
164
900
|
error("You can try again or use --use-existing-app-id with an existing app.");
|
|
165
|
-
|
|
166
|
-
|
|
901
|
+
// The template archive has been validated in a temp directory, but it has
|
|
902
|
+
// not been installed into targetDir yet (#1218).
|
|
903
|
+
warn("No project files were written. Re-run `primitive init` after resolving the app creation error.");
|
|
904
|
+
cleanupPreparedTemplate();
|
|
167
905
|
process.exit(1);
|
|
168
906
|
}
|
|
169
907
|
// Configure app access mode
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
908
|
+
let accessMode;
|
|
909
|
+
if (initConfig?.access_mode) {
|
|
910
|
+
accessMode = initConfig.access_mode;
|
|
911
|
+
info(`Access mode: ${accessMode} (from config)`);
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
console.log();
|
|
915
|
+
info("Let's configure who can access your app.");
|
|
916
|
+
info("(You can change this later in app.toml, then 'primitive config push --only app')");
|
|
917
|
+
accessMode = await select({
|
|
918
|
+
message: "Who should be able to sign up for your app?",
|
|
919
|
+
choices: [
|
|
920
|
+
{
|
|
921
|
+
value: "public",
|
|
922
|
+
name: "Anyone (public)",
|
|
923
|
+
description: "Anyone can create an account",
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
value: "domain",
|
|
927
|
+
name: "Domain restricted",
|
|
928
|
+
description: "Only users with email addresses from specific domains",
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
value: "invite-only",
|
|
932
|
+
name: "Invite only",
|
|
933
|
+
description: "Only users you explicitly invite can sign up",
|
|
934
|
+
},
|
|
935
|
+
],
|
|
936
|
+
default: "invite-only",
|
|
937
|
+
});
|
|
938
|
+
}
|
|
194
939
|
const client = new ApiClient();
|
|
195
940
|
const settingsPayload = {
|
|
196
941
|
mode: accessMode,
|
|
197
942
|
};
|
|
198
943
|
if (accessMode === "domain") {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
944
|
+
let allowedDomains;
|
|
945
|
+
if (initConfig?.allowed_domains && initConfig.allowed_domains.length > 0) {
|
|
946
|
+
allowedDomains = initConfig.allowed_domains;
|
|
947
|
+
info(`Allowed domains: ${allowedDomains.join(", ")} (from config)`);
|
|
948
|
+
}
|
|
949
|
+
else {
|
|
950
|
+
const domainsInput = await input({
|
|
951
|
+
message: "Enter allowed email domain(s) (comma-separated, e.g., 'acme.com, example.org'):",
|
|
952
|
+
validate: (value) => {
|
|
953
|
+
if (!value.trim())
|
|
954
|
+
return "Please enter at least one domain";
|
|
955
|
+
return true;
|
|
956
|
+
},
|
|
957
|
+
});
|
|
958
|
+
allowedDomains = domainsInput
|
|
959
|
+
.split(",")
|
|
960
|
+
.map((d) => d.trim().toLowerCase())
|
|
961
|
+
.filter((d) => d.length > 0);
|
|
962
|
+
}
|
|
211
963
|
settingsPayload.allowedDomains = allowedDomains;
|
|
212
964
|
}
|
|
213
965
|
// Update app settings with the chosen mode
|
|
@@ -218,34 +970,57 @@ async function handleInit(projectDirArg, options) {
|
|
|
218
970
|
catch (err) {
|
|
219
971
|
warn(`Could not update app settings: ${err.message}`);
|
|
220
972
|
}
|
|
221
|
-
// If invite-only, offer to add
|
|
973
|
+
// If invite-only, auto-add the authenticated user and offer to add others
|
|
222
974
|
if (accessMode === "invite-only") {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const emailsInput = await input({
|
|
229
|
-
message: "Enter email address(es) to allow (comma-separated):",
|
|
230
|
-
validate: (value) => {
|
|
231
|
-
if (!value.trim())
|
|
232
|
-
return "Please enter at least one email";
|
|
233
|
-
const emails = value.split(",").map((e) => e.trim());
|
|
234
|
-
for (const email of emails) {
|
|
235
|
-
if (!email.includes("@")) {
|
|
236
|
-
return `Invalid email: ${email}`;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return true;
|
|
240
|
-
},
|
|
975
|
+
try {
|
|
976
|
+
await client.createInvitation(appId, {
|
|
977
|
+
email: credentials.email,
|
|
978
|
+
role: "member",
|
|
979
|
+
sendEmail: false,
|
|
241
980
|
});
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
981
|
+
success(`${credentials.email} added to the invite list.`);
|
|
982
|
+
}
|
|
983
|
+
catch (err) {
|
|
984
|
+
if (!err.message?.includes("already has a pending invitation")) {
|
|
985
|
+
warn(`Could not add your email to the invite list: ${err.message}`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
// Determine emails to invite
|
|
989
|
+
let emailsToInvite = [];
|
|
990
|
+
if (initConfig?.invite_emails && initConfig.invite_emails.length > 0) {
|
|
991
|
+
emailsToInvite = initConfig.invite_emails;
|
|
992
|
+
info(`Inviting users from config: ${emailsToInvite.join(", ")}`);
|
|
993
|
+
}
|
|
994
|
+
else if (!initConfig) {
|
|
995
|
+
const inviteNow = await confirm({
|
|
996
|
+
message: "Would you like to add initial users who can sign up? (no emails will be sent)",
|
|
997
|
+
default: false,
|
|
998
|
+
});
|
|
999
|
+
if (inviteNow) {
|
|
1000
|
+
const emailsInput = await input({
|
|
1001
|
+
message: "Enter email address(es) to allow (comma-separated):",
|
|
1002
|
+
validate: (value) => {
|
|
1003
|
+
if (!value.trim())
|
|
1004
|
+
return "Please enter at least one email";
|
|
1005
|
+
const emails = value.split(",").map((e) => e.trim());
|
|
1006
|
+
for (const email of emails) {
|
|
1007
|
+
if (!email.includes("@")) {
|
|
1008
|
+
return `Invalid email: ${email}`;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return true;
|
|
1012
|
+
},
|
|
1013
|
+
});
|
|
1014
|
+
emailsToInvite = emailsInput
|
|
1015
|
+
.split(",")
|
|
1016
|
+
.map((e) => e.trim())
|
|
1017
|
+
.filter((e) => e.length > 0);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
if (emailsToInvite.length > 0) {
|
|
246
1021
|
info("Adding invited users...");
|
|
247
1022
|
let inviteCount = 0;
|
|
248
|
-
for (const email of
|
|
1023
|
+
for (const email of emailsToInvite) {
|
|
249
1024
|
try {
|
|
250
1025
|
await client.createInvitation(appId, {
|
|
251
1026
|
email,
|
|
@@ -264,98 +1039,808 @@ async function handleInit(projectDirArg, options) {
|
|
|
264
1039
|
}
|
|
265
1040
|
}
|
|
266
1041
|
}
|
|
267
|
-
//
|
|
268
|
-
|
|
1042
|
+
// ── Install every selected template ─────────────────────────────────
|
|
1043
|
+
//
|
|
1044
|
+
// The network download and archive validation already happened above,
|
|
1045
|
+
// without touching any target directory.
|
|
269
1046
|
try {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
1047
|
+
for (const client of plan.clients) {
|
|
1048
|
+
info(`Setting up your ${platformLabel(client.platform)} project...`);
|
|
1049
|
+
preparedTemplates.get(client.platform).install(client.dir);
|
|
1050
|
+
success(`${platformLabel(client.platform)} template extracted into ${client.dir}.`);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
catch (err) {
|
|
1054
|
+
error(`Failed to write template files: ${err.message}`);
|
|
1055
|
+
cleanupPreparedTemplate();
|
|
1056
|
+
process.exit(1);
|
|
1057
|
+
}
|
|
1058
|
+
finally {
|
|
1059
|
+
cleanupPreparedTemplate();
|
|
1060
|
+
}
|
|
1061
|
+
// Initialize git repository (commit happens later, after env + install).
|
|
1062
|
+
// An adopted repository already has one, and a nested repository inside it is
|
|
1063
|
+
// exactly what this change stops producing.
|
|
1064
|
+
if (plan.initsGit) {
|
|
1065
|
+
try {
|
|
1066
|
+
await gitInitRepo(plan.projectRoot);
|
|
274
1067
|
}
|
|
275
|
-
|
|
276
|
-
|
|
1068
|
+
catch {
|
|
1069
|
+
// Non-fatal: git may not be installed, or already a git repo
|
|
277
1070
|
}
|
|
278
|
-
|
|
279
|
-
|
|
1071
|
+
}
|
|
1072
|
+
// Step 4.6: Scaffold primitive/config.json with a "dev" environment bound
|
|
1073
|
+
// to this app, and seed its credential slot with the session we just
|
|
1074
|
+
// authenticated — so `primitive` commands run from inside the project pick
|
|
1075
|
+
// up project mode without a second login.
|
|
1076
|
+
if (plan.writesProjectConfig) {
|
|
1077
|
+
try {
|
|
1078
|
+
await scaffoldProjectConfig({
|
|
1079
|
+
targetDir: plan.projectRoot,
|
|
1080
|
+
appId,
|
|
1081
|
+
appName,
|
|
1082
|
+
serverUrl,
|
|
1083
|
+
});
|
|
1084
|
+
if (credentials) {
|
|
1085
|
+
seedCredentialsForEnvInProject(plan.projectRoot, "dev", credentials);
|
|
1086
|
+
}
|
|
1087
|
+
success(`Project environment "dev" configured (primitive/config.json).`);
|
|
1088
|
+
}
|
|
1089
|
+
catch (err) {
|
|
1090
|
+
warn(`Could not scaffold primitive/config.json: ${err.message}`);
|
|
280
1091
|
}
|
|
281
1092
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
|
|
1093
|
+
else {
|
|
1094
|
+
// Adopting: the config is the project's own. Only make sure the local state
|
|
1095
|
+
// a new client's environment produces stays out of the repository.
|
|
1096
|
+
try {
|
|
1097
|
+
ensureGitignoreLocalState(plan.projectRoot);
|
|
1098
|
+
}
|
|
1099
|
+
catch (err) {
|
|
1100
|
+
warn(`Could not update .gitignore: ${err.message}`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
// ── The one model schema (#2890) ────────────────────────────────────
|
|
1104
|
+
if (plan.canonicalSchemaPath && schemaPlan) {
|
|
1105
|
+
composeSchema({ plan, schemaPlan });
|
|
1106
|
+
}
|
|
1107
|
+
// ── The app-wide AGENTS.md ──────────────────────────────────────────
|
|
1108
|
+
if (plan.writesRootAgents) {
|
|
1109
|
+
const agentsPath = join(plan.projectRoot, "AGENTS.md");
|
|
1110
|
+
if (!existsSync(agentsPath)) {
|
|
297
1111
|
try {
|
|
298
|
-
|
|
299
|
-
|
|
1112
|
+
// The clients this run wrote, plus any the repository already had —
|
|
1113
|
+
// adding iOS to a web app describes a repository with two clients.
|
|
1114
|
+
const written = plan.clients.map((client) => ({
|
|
1115
|
+
platform: client.platform,
|
|
1116
|
+
dir: relative(plan.projectRoot, client.dir) || ".",
|
|
1117
|
+
}));
|
|
1118
|
+
const clients = [
|
|
1119
|
+
...written,
|
|
1120
|
+
...discoverExistingClients(plan.projectRoot).filter((existing) => !written.some((client) => client.dir === existing.dir)),
|
|
1121
|
+
];
|
|
1122
|
+
writeFileSync(agentsPath, rootAgentsMarkdown({ appName, clients }));
|
|
1123
|
+
success("Wrote AGENTS.md describing the app layout.");
|
|
300
1124
|
}
|
|
301
1125
|
catch (err) {
|
|
302
|
-
warn(`Could not
|
|
303
|
-
warn("Will use npx to run pnpm for dependency installation.");
|
|
1126
|
+
warn(`Could not write AGENTS.md: ${err.message}`);
|
|
304
1127
|
}
|
|
305
1128
|
}
|
|
306
1129
|
}
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
|
|
1130
|
+
// ── Per-client configuration and dependency installation ────────────
|
|
1131
|
+
//
|
|
1132
|
+
// The production runtime target is derived once from the project's own
|
|
1133
|
+
// environments, so a client built for production talks to a backend the app
|
|
1134
|
+
// id actually exists on (#2890).
|
|
1135
|
+
const production = plan.mode === "standalone"
|
|
1136
|
+
? undefined
|
|
1137
|
+
: resolveProductionRuntime({
|
|
1138
|
+
environments: (adopted?.projectConfig.environments ??
|
|
1139
|
+
readProjectEnvironments(plan.projectRoot)),
|
|
1140
|
+
selectedServerUrl: serverUrl,
|
|
1141
|
+
selectedAppId: appId,
|
|
1142
|
+
selectedAppName: appName,
|
|
1143
|
+
productionServerUrl: PRODUCTION_SERVER_URL,
|
|
1144
|
+
});
|
|
1145
|
+
/**
|
|
1146
|
+
* The URL scheme derived for each Apple client, for the final summary.
|
|
1147
|
+
* `registered` is false when the client's plist had no `PrimitiveAuth` URL
|
|
1148
|
+
* type to stamp it into, so the summary and the opt-in guidance describe a
|
|
1149
|
+
* candidate scheme rather than a registered one.
|
|
1150
|
+
*/
|
|
1151
|
+
const iosCallbackSchemes = [];
|
|
1152
|
+
/**
|
|
1153
|
+
* An app with BOTH clients emails ONE https sign-in link — the web app's
|
|
1154
|
+
* callback, which opens the iOS app where it is installed (#2982). "Both"
|
|
1155
|
+
* counts the clients already in the repository, including one that IS the
|
|
1156
|
+
* project root (the flat single-client layout gaining its second platform).
|
|
1157
|
+
*/
|
|
1158
|
+
const combinedApp = appHasBothPlatforms(plan.clients.map((client) => client.platform), discoverClientPlatforms(plan.projectRoot));
|
|
1159
|
+
/** The web dev origin this run settled on, for the environment's `webUrl`. */
|
|
1160
|
+
let webDevOrigin = null;
|
|
1161
|
+
/**
|
|
1162
|
+
* Whether that origin's `/oauth/callback` is known to be on the app's
|
|
1163
|
+
* magic-link allow-list already — because the app was created here with the
|
|
1164
|
+
* default-port seed, or because the dev-port step merged it (#2982). Anything
|
|
1165
|
+
* else has to be asked about before the environment points a sign-in link at
|
|
1166
|
+
* it.
|
|
1167
|
+
*/
|
|
1168
|
+
let devCallbackAllowListed = false;
|
|
1169
|
+
for (const client of plan.clients) {
|
|
1170
|
+
if (client.platform === "ios") {
|
|
1171
|
+
// ── Apple (SwiftUI) path ────────────────────────────────────────
|
|
1172
|
+
// The Xcode build phases name the project config as a declared input,
|
|
1173
|
+
// and inside Xcode's script sandbox a phase may read only what it
|
|
1174
|
+
// declared. When the config lives above this client, both the generated
|
|
1175
|
+
// spec and the checked-in .xcodeproj have to say so — otherwise a fresh
|
|
1176
|
+
// clone (primitive.json is gitignored) cannot build in Xcode at all, and
|
|
1177
|
+
// a working copy that has one keeps whichever environment wrote it.
|
|
1178
|
+
if (resolve(client.dir) !== resolve(plan.projectRoot)) {
|
|
1179
|
+
const declared = declareSharedProjectConfig(client.dir, plan.projectRoot);
|
|
1180
|
+
if (declared.length > 0) {
|
|
1181
|
+
success(`Xcode build phases read ${relative(client.dir, join(plan.projectRoot, PROJECT_TREE_DIR, PROJECT_CONFIG_FILENAME))}.`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
// Step 5: Generate primitive.json from the environment.
|
|
1185
|
+
//
|
|
1186
|
+
// In a current template the file is a build product, written by
|
|
1187
|
+
// `scripts/resolve-primitive-config.sh` from `primitive/config.json`
|
|
1188
|
+
// (#2873) — it walks up to the project root, so a client inside a
|
|
1189
|
+
// composed repository resolves the same one every other command does. A
|
|
1190
|
+
// template that predates the change has no such script and a tracked
|
|
1191
|
+
// primitive.json instead; fill that in the old way.
|
|
1192
|
+
info("Configuring primitive.json...");
|
|
1193
|
+
try {
|
|
1194
|
+
const result = configurePrimitiveJson(client.dir, appId, serverUrl, appName, {
|
|
1195
|
+
// Adopting resolved an environment of the project's own — the one
|
|
1196
|
+
// whose app this client is being added to. The script has to be told
|
|
1197
|
+
// which, or it re-resolves from this machine and can configure the
|
|
1198
|
+
// client for a different app on a different backend.
|
|
1199
|
+
environmentName: adopted?.name,
|
|
1200
|
+
});
|
|
1201
|
+
if (result.outcome === "generated") {
|
|
1202
|
+
success("primitive.json generated from primitive/config.json.");
|
|
1203
|
+
}
|
|
1204
|
+
else if (result.outcome === "filled") {
|
|
1205
|
+
success("primitive.json configured.");
|
|
1206
|
+
}
|
|
1207
|
+
else if (result.outcome === "failed") {
|
|
1208
|
+
warn(`Could not generate primitive.json: ${result.detail}`);
|
|
1209
|
+
warn("Run 'bash scripts/resolve-primitive-config.sh' in the project to retry.");
|
|
1210
|
+
}
|
|
1211
|
+
else {
|
|
1212
|
+
// Neither the resolve script nor a template primitive.json — an
|
|
1213
|
+
// unexpectedly old or trimmed template. The remedy is still never
|
|
1214
|
+
// "type the app ID somewhere else": it is already in
|
|
1215
|
+
// primitive/config.json, which is where every reader looks.
|
|
1216
|
+
warn("This template has neither scripts/resolve-primitive-config.sh nor a primitive.json.");
|
|
1217
|
+
warn(`The project config holds the app ID; update the template to generate primitive.json from it.`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
catch (err) {
|
|
1221
|
+
warn(`Could not configure primitive.json: ${err.message}`);
|
|
1222
|
+
warn("Run 'bash scripts/resolve-primitive-config.sh' in the project to retry.");
|
|
1223
|
+
}
|
|
1224
|
+
// Step 5b: Give this client its own URL scheme (#2969).
|
|
1225
|
+
//
|
|
1226
|
+
// The template ships `primitiveapp` registered, which every app
|
|
1227
|
+
// scaffolded from it would otherwise share — iOS then picks arbitrarily
|
|
1228
|
+
// between two of them for an incoming link. The stamped scheme carries
|
|
1229
|
+
// the app id, so no two apps collide.
|
|
1230
|
+
//
|
|
1231
|
+
// Nothing is written to the app's `emailRedirectUris`, here or on the
|
|
1232
|
+
// server: the manager sends no redirect target by default, so this app's
|
|
1233
|
+
// email sign-in works immediately and code-only. The link is the
|
|
1234
|
+
// developer's opt-in, and this run only prints what it takes.
|
|
1235
|
+
const callbackScheme = deriveCallbackScheme(appName, appId, basename(client.dir));
|
|
1236
|
+
const stamped = stampCallbackScheme(client.dir, callbackScheme);
|
|
1237
|
+
iosCallbackSchemes.push({
|
|
1238
|
+
dir: client.dir,
|
|
1239
|
+
scheme: callbackScheme,
|
|
1240
|
+
registered: stamped.outcome !== "not-found",
|
|
1241
|
+
});
|
|
1242
|
+
if (stamped.outcome === "not-found") {
|
|
1243
|
+
for (const line of missingUrlTypeGuidance(callbackScheme))
|
|
1244
|
+
warn(line);
|
|
1245
|
+
}
|
|
1246
|
+
else {
|
|
1247
|
+
success(`Callback URL scheme registered: ${callbackScheme}`);
|
|
1248
|
+
}
|
|
1249
|
+
// Step 6-7: Skip port selection and pnpm (not applicable to Apple)
|
|
1250
|
+
// Step 8: Resolve Swift packages
|
|
1251
|
+
if (!skipInstall) {
|
|
1252
|
+
info("Resolving Swift packages (this may take a few minutes)...");
|
|
1253
|
+
try {
|
|
1254
|
+
await runSwiftPackageResolve(client.dir);
|
|
1255
|
+
success("Swift packages resolved.");
|
|
1256
|
+
}
|
|
1257
|
+
catch (err) {
|
|
1258
|
+
warn(`Swift package resolution failed: ${err.message}`);
|
|
1259
|
+
warn("You can try running 'swift package resolve' manually.");
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
else {
|
|
1263
|
+
info(`Skipping dependency installation (${skipInstallReason}).`);
|
|
1264
|
+
}
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
// ── Web path ──────────────────────────────────────────────────────
|
|
1268
|
+
// Step 5: Fill in an OLD template's .env identity keys.
|
|
1269
|
+
//
|
|
1270
|
+
// A current template does not author them at all — `primitiveEnv()` reads
|
|
1271
|
+
// `primitive/config.json`, which it finds by walking up — so "no keys to
|
|
1272
|
+
// replace" is the expected outcome, not a problem (#2873). The fill-in
|
|
1273
|
+
// survives only so a new CLI still produces a working scaffold from a
|
|
1274
|
+
// template that predates the change.
|
|
1275
|
+
info("Configuring environment...");
|
|
310
1276
|
try {
|
|
311
|
-
|
|
312
|
-
|
|
1277
|
+
const envResult = updateEnvFile(client.dir, appId, serverUrl, appName, production);
|
|
1278
|
+
if (!envResult.envUpdated) {
|
|
1279
|
+
info("No .env file in this template — identity comes from primitive/config.json.");
|
|
1280
|
+
}
|
|
1281
|
+
else {
|
|
1282
|
+
success("Environment configured.");
|
|
1283
|
+
}
|
|
313
1284
|
}
|
|
314
1285
|
catch (err) {
|
|
315
|
-
warn(`
|
|
316
|
-
|
|
1286
|
+
warn(`Could not update .env file: ${err.message}`);
|
|
1287
|
+
}
|
|
1288
|
+
// Step 6: Determine the dev port. Default to 5173; warn about taken ports
|
|
1289
|
+
// when prompting. The init config's `dev_port` decides it non-interactively
|
|
1290
|
+
// — this was the one branch in the command that prompted unconditionally,
|
|
1291
|
+
// so the documented key was ignored and a closed stdin threw
|
|
1292
|
+
// ERR_USE_AFTER_CLOSE into the catch below (#2720).
|
|
1293
|
+
try {
|
|
1294
|
+
const defaultPort = DEFAULT_DEV_PORT;
|
|
1295
|
+
const defaultOrigin = localDevOrigin(defaultPort);
|
|
1296
|
+
const resolvedPort = await resolveDevPort({
|
|
1297
|
+
configPort: initConfig?.dev_port,
|
|
1298
|
+
hasConfig: initConfig != null,
|
|
1299
|
+
isTTY: Boolean(process.stdin.isTTY),
|
|
1300
|
+
defaultPort,
|
|
1301
|
+
promptFn: async () => {
|
|
1302
|
+
console.log();
|
|
1303
|
+
// Scan up to 10 ports starting from 5173 to surface what's already in use
|
|
1304
|
+
const takenPorts = [];
|
|
1305
|
+
for (let p = defaultPort; p < defaultPort + 10; p++) {
|
|
1306
|
+
if (!(await isPortFree(p)))
|
|
1307
|
+
takenPorts.push(p);
|
|
1308
|
+
else
|
|
1309
|
+
break;
|
|
1310
|
+
}
|
|
1311
|
+
if (takenPorts.length > 0) {
|
|
1312
|
+
warn(`Port${takenPorts.length > 1 ? "s" : ""} ${takenPorts.join(", ")} ${takenPorts.length > 1 ? "are" : "is"} already in use.`);
|
|
1313
|
+
}
|
|
1314
|
+
const portInput = await input({
|
|
1315
|
+
message: "Dev server port for this project:",
|
|
1316
|
+
default: String(defaultPort),
|
|
1317
|
+
validate: validateDevPort,
|
|
1318
|
+
});
|
|
1319
|
+
return parseInt(portInput, 10);
|
|
1320
|
+
},
|
|
1321
|
+
});
|
|
1322
|
+
const chosenPort = resolvedPort.port;
|
|
1323
|
+
// The same origin the app's CORS list and `emailRedirectUris` are seeded
|
|
1324
|
+
// with, so a combined app's first sign-in link is already allow-listed.
|
|
1325
|
+
webDevOrigin = localDevOrigin(chosenPort);
|
|
1326
|
+
// …but only for an app this run created: that seed happens at creation.
|
|
1327
|
+
// An adopted app on the default port has been through neither the seed
|
|
1328
|
+
// nor the port step below, so nothing here can vouch for its allow-list.
|
|
1329
|
+
devCallbackAllowListed = appWasCreatedByThisRun && chosenPort === defaultPort;
|
|
1330
|
+
if (resolvedPort.source === "config") {
|
|
1331
|
+
info(`Dev server port: ${chosenPort} (from config)`);
|
|
1332
|
+
}
|
|
1333
|
+
if (chosenPort !== defaultPort) {
|
|
1334
|
+
const devOrigin = localDevOrigin(chosenPort);
|
|
1335
|
+
// Pin the port in vite.config.ts and update .env. A vite config whose
|
|
1336
|
+
// shape we can't patch must be said out loud — otherwise the dev
|
|
1337
|
+
// server keeps 5173 while .env and the redirect URIs point elsewhere.
|
|
1338
|
+
const viteUpdated = updateViteConfig(client.dir, chosenPort);
|
|
1339
|
+
if (!viteUpdated) {
|
|
1340
|
+
warn(`Could not pin port ${chosenPort} in vite.config.ts — set \`server.port\` there manually.`);
|
|
1341
|
+
}
|
|
1342
|
+
updateEnvPort(client.dir, chosenPort);
|
|
1343
|
+
// Update app settings: CORS, redirect URIs, baseUrl
|
|
1344
|
+
if (!credentials) {
|
|
1345
|
+
info(`Local files updated. Skipping server CORS/redirect config — not authenticated.`);
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
try {
|
|
1349
|
+
const apiClient = new ApiClient();
|
|
1350
|
+
let existingOrigins = [];
|
|
1351
|
+
let existingEmailRedirectUris = [];
|
|
1352
|
+
let existingGoogleClients = null;
|
|
1353
|
+
try {
|
|
1354
|
+
const currentSettings = await apiClient.getAppSettings(appId);
|
|
1355
|
+
if (currentSettings.corsMode === "custom" &&
|
|
1356
|
+
Array.isArray(currentSettings.corsAllowedOrigins)) {
|
|
1357
|
+
existingOrigins = currentSettings.corsAllowedOrigins;
|
|
1358
|
+
}
|
|
1359
|
+
if (Array.isArray(currentSettings.emailRedirectUris)) {
|
|
1360
|
+
existingEmailRedirectUris = currentSettings.emailRedirectUris;
|
|
1361
|
+
}
|
|
1362
|
+
if (currentSettings.googleClients) {
|
|
1363
|
+
existingGoogleClients = currentSettings.googleClients;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
catch {
|
|
1367
|
+
// Ignore — will set a fresh config
|
|
1368
|
+
}
|
|
1369
|
+
// Always include 5173 since it's the universal localhost default
|
|
1370
|
+
const corsOrigins = Array.from(new Set([...existingOrigins, defaultOrigin, devOrigin]));
|
|
1371
|
+
// MERGED into the creation-time seed, never replacing it (#2891):
|
|
1372
|
+
// `emailRedirectUris` matching is fail-closed, so blindly writing
|
|
1373
|
+
// only this port's callback would break every other environment the
|
|
1374
|
+
// app already serves.
|
|
1375
|
+
const emailRedirectUris = Array.from(new Set([
|
|
1376
|
+
...existingEmailRedirectUris,
|
|
1377
|
+
`${defaultOrigin}/oauth/callback`,
|
|
1378
|
+
`${devOrigin}/oauth/callback`,
|
|
1379
|
+
]));
|
|
1380
|
+
const settingsUpdate = {
|
|
1381
|
+
corsMode: "custom",
|
|
1382
|
+
corsAllowedOrigins: corsOrigins,
|
|
1383
|
+
corsAllowCredentials: true,
|
|
1384
|
+
emailRedirectUris,
|
|
1385
|
+
baseUrl: devOrigin,
|
|
1386
|
+
};
|
|
1387
|
+
// A Google web client redirects to this dev origin too, so its own
|
|
1388
|
+
// `redirectUris` needs the callback. A PUT replaces the WHOLE map,
|
|
1389
|
+
// so the complete map is sent back — an entry for a platform this
|
|
1390
|
+
// scaffold does not touch (ios, desktop, …) would otherwise be
|
|
1391
|
+
// deleted as a side effect of choosing a dev port.
|
|
1392
|
+
const webEntry = existingGoogleClients?.clients?.web;
|
|
1393
|
+
if (webEntry) {
|
|
1394
|
+
settingsUpdate.googleClients = {
|
|
1395
|
+
...existingGoogleClients,
|
|
1396
|
+
clients: {
|
|
1397
|
+
...existingGoogleClients.clients,
|
|
1398
|
+
web: {
|
|
1399
|
+
...webEntry,
|
|
1400
|
+
redirectUris: Array.from(new Set([
|
|
1401
|
+
...(webEntry.redirectUris ?? []),
|
|
1402
|
+
`${defaultOrigin}/oauth/callback`,
|
|
1403
|
+
`${devOrigin}/oauth/callback`,
|
|
1404
|
+
])),
|
|
1405
|
+
},
|
|
1406
|
+
},
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
await apiClient.updateAppSettings(appId, settingsUpdate);
|
|
1410
|
+
// This merge is what puts the chosen port's callback on the
|
|
1411
|
+
// allow-list, whoever created the app (#2982).
|
|
1412
|
+
devCallbackAllowListed = true;
|
|
1413
|
+
success(`Dev port set to ${chosenPort}: ${viteUpdated ? "vite.config.ts, " : ""}.env, CORS, redirect URIs, and base URL updated.`);
|
|
1414
|
+
}
|
|
1415
|
+
catch (err) {
|
|
1416
|
+
warn(`Could not update app settings: ${err.message}`);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
catch (err) {
|
|
1422
|
+
// Non-fatal — but say so. A silent swallow here is what hid the closed
|
|
1423
|
+
// stdin crash in #2720: the run kept 5173 and still exited 0.
|
|
1424
|
+
warn(`Could not configure the dev server port: ${err.message}`);
|
|
1425
|
+
warn("The project keeps the default port 5173.");
|
|
1426
|
+
}
|
|
1427
|
+
// Step 7: Check if pnpm is installed globally, offer to install if not.
|
|
1428
|
+
//
|
|
1429
|
+
// An offer, so only where there is someone to ask: a config-driven run
|
|
1430
|
+
// answers no questions, and a piped stdin has no answer to give. Either way
|
|
1431
|
+
// the install below still runs pnpm through npx, so the client gets its
|
|
1432
|
+
// dependencies — this is about the machine, not about this project.
|
|
1433
|
+
const pnpmInstalled = await isPnpmInstalled();
|
|
1434
|
+
if (!pnpmInstalled) {
|
|
1435
|
+
console.log();
|
|
1436
|
+
info("pnpm is not installed globally on your system.");
|
|
1437
|
+
const canAsk = initConfig == null && Boolean(process.stdin.isTTY);
|
|
1438
|
+
const installPnpm = canAsk
|
|
1439
|
+
? await confirm({
|
|
1440
|
+
message: "Install pnpm globally? (recommended for future use)",
|
|
1441
|
+
default: true,
|
|
1442
|
+
})
|
|
1443
|
+
: false;
|
|
1444
|
+
if (!canAsk) {
|
|
1445
|
+
info("Using npx to run pnpm. Install it globally later with: npm install -g pnpm");
|
|
1446
|
+
}
|
|
1447
|
+
if (installPnpm) {
|
|
1448
|
+
info("Installing pnpm...");
|
|
1449
|
+
try {
|
|
1450
|
+
await installPnpmGlobally();
|
|
1451
|
+
success("pnpm installed globally. You can now use 'pnpm' commands.");
|
|
1452
|
+
}
|
|
1453
|
+
catch (err) {
|
|
1454
|
+
warn(`Could not install pnpm: ${err.message}`);
|
|
1455
|
+
warn("Will use npx to run pnpm for dependency installation.");
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
// Step 8: Install dependencies with pnpm (via npx to handle version management)
|
|
1460
|
+
if (!skipInstall) {
|
|
1461
|
+
info("Installing dependencies...");
|
|
1462
|
+
try {
|
|
1463
|
+
await runPnpmInstall(client.dir);
|
|
1464
|
+
success("Dependencies installed.");
|
|
1465
|
+
}
|
|
1466
|
+
catch (err) {
|
|
1467
|
+
warn(`Dependency installation failed: ${err.message}`);
|
|
1468
|
+
warn("You can try running 'npx pnpm install' manually.");
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
else {
|
|
1472
|
+
info(`Skipping dependency installation (${skipInstallReason}).`);
|
|
317
1473
|
}
|
|
318
1474
|
}
|
|
319
|
-
|
|
320
|
-
info("Skipping dependency installation (--skip-install).");
|
|
321
|
-
}
|
|
322
|
-
// Step 8: Install primitive CLI globally if not already installed
|
|
1475
|
+
// Step 9: Install primitive CLI globally if not already installed
|
|
323
1476
|
const cliInstalled = await isPrimitiveCliInstalled();
|
|
324
1477
|
if (!cliInstalled) {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
const installCli = await confirm({
|
|
328
|
-
message: "Install the Primitive CLI globally?",
|
|
329
|
-
default: true,
|
|
330
|
-
});
|
|
331
|
-
if (installCli) {
|
|
1478
|
+
if (initConfig) {
|
|
1479
|
+
// Non-interactive: auto-install CLI
|
|
332
1480
|
info("Installing Primitive CLI...");
|
|
333
1481
|
try {
|
|
334
1482
|
await installPrimitiveCli();
|
|
335
|
-
success("Primitive CLI installed.
|
|
1483
|
+
success("Primitive CLI installed.");
|
|
336
1484
|
}
|
|
337
1485
|
catch (err) {
|
|
338
1486
|
warn(`Could not install CLI: ${err.message}`);
|
|
339
|
-
warn(
|
|
1487
|
+
warn(`You can install it manually with: ${installCommand(PRIMITIVE_CLI_PACKAGE)}`);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
else {
|
|
1491
|
+
console.log();
|
|
1492
|
+
info("The Primitive CLI helps you manage your app from the command line.");
|
|
1493
|
+
const installCli = await confirm({
|
|
1494
|
+
message: "Install the Primitive CLI globally?",
|
|
1495
|
+
default: true,
|
|
1496
|
+
});
|
|
1497
|
+
if (installCli) {
|
|
1498
|
+
info("Installing Primitive CLI...");
|
|
1499
|
+
try {
|
|
1500
|
+
await installPrimitiveCli();
|
|
1501
|
+
success("Primitive CLI installed. You can now use 'primitive' commands.");
|
|
1502
|
+
}
|
|
1503
|
+
catch (err) {
|
|
1504
|
+
warn(`Could not install CLI: ${err.message}`);
|
|
1505
|
+
warn(`You can install it manually with: ${installCommand(PRIMITIVE_CLI_PACKAGE)}`);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
// The combined app's dev sign-in link (#2982): the environment learns the
|
|
1511
|
+
// web origin this run settled on, so the iOS client's emailed link opens the
|
|
1512
|
+
// local web app instead of a scheme that is dead in the Simulator. Only when
|
|
1513
|
+
// this run owns the project config — an adopting run gets the guidance and
|
|
1514
|
+
// decides for itself.
|
|
1515
|
+
//
|
|
1516
|
+
// And only once the app actually allows that callback. Setting `webUrl` is
|
|
1517
|
+
// what turns a code-only email into a link, and a link the server refuses is
|
|
1518
|
+
// a 400 on every sign-in request — strictly worse than the code-only default
|
|
1519
|
+
// this app has today. An app created here was seeded with the callback; an
|
|
1520
|
+
// adopted one is asked about, and when the answer is no or unavailable the
|
|
1521
|
+
// seed is skipped in favour of printing the step.
|
|
1522
|
+
let seededWebOrigin = null;
|
|
1523
|
+
let unverifiedDevCallback = null;
|
|
1524
|
+
if (combinedApp && plan.writesProjectConfig && webDevOrigin) {
|
|
1525
|
+
const devCallback = `${webDevOrigin}/oauth/callback`;
|
|
1526
|
+
const allowed = devCallbackAllowListed ||
|
|
1527
|
+
(await devCallbackIsAllowListed(appId, devCallback, credentials));
|
|
1528
|
+
if (allowed && seedEnvironmentWebUrl(plan.projectRoot, "dev", webDevOrigin)) {
|
|
1529
|
+
seededWebOrigin = webDevOrigin;
|
|
1530
|
+
success(`Environment "dev" points at the web client: webUrl ${webDevOrigin}`);
|
|
1531
|
+
}
|
|
1532
|
+
else if (!allowed) {
|
|
1533
|
+
unverifiedDevCallback = devCallback;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
// Step 10: Offer to install Claude Code skill if not already installed
|
|
1537
|
+
try {
|
|
1538
|
+
const { isSkillInstalled, installSkillExplicit, checkSkillStatus } = await import("../lib/skill-installer.js");
|
|
1539
|
+
if (isSkillInstalled()) {
|
|
1540
|
+
// Already installed — silently auto-update if a newer version is bundled
|
|
1541
|
+
await checkSkillStatus();
|
|
1542
|
+
}
|
|
1543
|
+
else if (initConfig) {
|
|
1544
|
+
// Non-interactive: auto-install skill
|
|
1545
|
+
await installSkillExplicit();
|
|
1546
|
+
}
|
|
1547
|
+
else {
|
|
1548
|
+
console.log();
|
|
1549
|
+
info("The Primitive platform skill gives Claude Code expertise with Primitive APIs and patterns.");
|
|
1550
|
+
const installSkill = await confirm({
|
|
1551
|
+
message: "Install the Primitive platform skill for Claude Code?",
|
|
1552
|
+
default: true,
|
|
1553
|
+
});
|
|
1554
|
+
if (installSkill) {
|
|
1555
|
+
await installSkillExplicit();
|
|
340
1556
|
}
|
|
341
1557
|
}
|
|
342
1558
|
}
|
|
343
|
-
|
|
1559
|
+
catch {
|
|
1560
|
+
// Non-fatal: skill installation should never block init
|
|
1561
|
+
}
|
|
1562
|
+
// Step 11: Create initial git commit (captures env config + installed
|
|
1563
|
+
// dependencies). Not when adopting: the surrounding repository owns its
|
|
1564
|
+
// history, and the new files are the user's to review and commit.
|
|
1565
|
+
if (plan.initsGit) {
|
|
1566
|
+
try {
|
|
1567
|
+
await gitInitialCommit(plan.projectRoot);
|
|
1568
|
+
}
|
|
1569
|
+
catch {
|
|
1570
|
+
// Non-fatal: git may not be installed, or already committed
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
// Step 12: Show success message
|
|
344
1574
|
divider();
|
|
345
|
-
success(
|
|
1575
|
+
success(plan.mode === "add-client"
|
|
1576
|
+
? "Client added successfully!"
|
|
1577
|
+
: "Project initialized successfully!");
|
|
346
1578
|
divider();
|
|
347
|
-
keyValue("
|
|
1579
|
+
keyValue("Project", plan.projectRoot);
|
|
348
1580
|
keyValue("App Name", appName);
|
|
349
1581
|
keyValue("App ID", appId);
|
|
1582
|
+
for (const client of plan.clients) {
|
|
1583
|
+
keyValue(platformLabel(client.platform), client.dir);
|
|
1584
|
+
}
|
|
1585
|
+
if (plan.canonicalSchemaPath) {
|
|
1586
|
+
keyValue("Model schema", plan.canonicalSchemaPath);
|
|
1587
|
+
}
|
|
1588
|
+
for (const { scheme, registered } of iosCallbackSchemes) {
|
|
1589
|
+
keyValue("Callback URL scheme", registered ? scheme : `${scheme} (not registered — see the warning above)`);
|
|
1590
|
+
}
|
|
350
1591
|
divider();
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
1592
|
+
// Email sign-in already works for the scaffolded app — code-only, no app
|
|
1593
|
+
// settings — so this is an opt-in, not a step (#2969). An app with both
|
|
1594
|
+
// clients gets the https link instead, which needs a different checklist
|
|
1595
|
+
// (#2982).
|
|
1596
|
+
if (combinedApp) {
|
|
1597
|
+
for (const line of universalLinkGuidance({
|
|
1598
|
+
webOrigin: seededWebOrigin ?? undefined,
|
|
1599
|
+
unverifiedDevCallback: unverifiedDevCallback ?? undefined,
|
|
1600
|
+
})) {
|
|
1601
|
+
info(line);
|
|
1602
|
+
}
|
|
1603
|
+
divider();
|
|
354
1604
|
}
|
|
355
|
-
|
|
356
|
-
|
|
1605
|
+
else {
|
|
1606
|
+
for (const { scheme, registered } of iosCallbackSchemes) {
|
|
1607
|
+
for (const line of linkOptInGuidance(scheme, { registered }))
|
|
1608
|
+
info(line);
|
|
1609
|
+
divider();
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
if (plan.mode === "add-client") {
|
|
1613
|
+
info(`No .primitive/ and no .git/ were created inside the new client — this app has one of each, at ${plan.projectRoot}.`);
|
|
1614
|
+
info(`The new files are uncommitted: review them with \`git status\` in ${plan.projectRoot} and commit when you are happy.`);
|
|
1615
|
+
divider();
|
|
1616
|
+
}
|
|
1617
|
+
info("Next steps:");
|
|
1618
|
+
for (const client of plan.clients) {
|
|
1619
|
+
const relativeDir = relative(process.cwd(), client.dir);
|
|
1620
|
+
// A target outside the working directory reads better as its own path than
|
|
1621
|
+
// as a chain of `../`.
|
|
1622
|
+
const where = relativeDir.startsWith("..") ? client.dir : relativeDir || ".";
|
|
1623
|
+
if (where !== ".")
|
|
1624
|
+
console.log(` cd ${where}`);
|
|
1625
|
+
if (client.platform === "ios") {
|
|
1626
|
+
if (skipInstall)
|
|
1627
|
+
console.log(" swift package resolve");
|
|
1628
|
+
// The Xcode project lists its sources explicitly, and the shared schema
|
|
1629
|
+
// brings models the checked-in project does not compile yet — so the
|
|
1630
|
+
// first build regenerates. `run-ios.sh` runs codegen and xcodegen in that
|
|
1631
|
+
// order; a build straight from Xcode before that stops and says so.
|
|
1632
|
+
if (resolve(client.dir) !== resolve(plan.projectRoot)) {
|
|
1633
|
+
console.log(" ./run-ios.sh # codegen + xcodegen, then the Simulator");
|
|
1634
|
+
console.log(" # then: open *.xcodeproj");
|
|
1635
|
+
}
|
|
1636
|
+
else {
|
|
1637
|
+
console.log(" open *.xcodeproj");
|
|
1638
|
+
console.log(" # or: ./run-ios.sh (iOS Simulator)");
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
else {
|
|
1642
|
+
if (skipInstall)
|
|
1643
|
+
console.log(" pnpm install");
|
|
1644
|
+
console.log(" pnpm dev");
|
|
1645
|
+
}
|
|
357
1646
|
}
|
|
358
|
-
console.log(" pnpm run dev");
|
|
359
1647
|
console.log();
|
|
360
1648
|
}
|
|
1649
|
+
/** Where a project root's environments live, for the production derivation. */
|
|
1650
|
+
function readProjectEnvironments(projectRoot) {
|
|
1651
|
+
const configPath = join(projectRoot, PROJECT_TREE_DIR, PROJECT_CONFIG_FILENAME);
|
|
1652
|
+
if (!existsSync(configPath))
|
|
1653
|
+
return undefined;
|
|
1654
|
+
try {
|
|
1655
|
+
return loadProjectConfigFromPath(configPath).environments;
|
|
1656
|
+
}
|
|
1657
|
+
catch {
|
|
1658
|
+
return undefined;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* The clients a repository already has, for the app-wide `AGENTS.md`.
|
|
1663
|
+
*
|
|
1664
|
+
* A run that adds a client to an existing app knows only about the one it is
|
|
1665
|
+
* writing, and a layout description that names one client of two describes the
|
|
1666
|
+
* repository wrongly to the next agent that reads it. The signals are the ones
|
|
1667
|
+
* the templates ship — a Swift package or Xcode project, a Vite project — so a
|
|
1668
|
+
* `functions/` directory with a `package.json` is not mistaken for a client.
|
|
1669
|
+
*/
|
|
1670
|
+
function discoverExistingClients(projectRoot) {
|
|
1671
|
+
let entries;
|
|
1672
|
+
try {
|
|
1673
|
+
entries = readdirSync(projectRoot, { withFileTypes: true });
|
|
1674
|
+
}
|
|
1675
|
+
catch {
|
|
1676
|
+
return [];
|
|
1677
|
+
}
|
|
1678
|
+
const found = [];
|
|
1679
|
+
for (const entry of entries) {
|
|
1680
|
+
if (!entry.isDirectory() || entry.name.startsWith("."))
|
|
1681
|
+
continue;
|
|
1682
|
+
// The same template signals the both-platforms predicate reads, so the
|
|
1683
|
+
// layout description and the universal-link wiring cannot disagree about
|
|
1684
|
+
// what counts as a client.
|
|
1685
|
+
const platform = detectClientPlatform(join(projectRoot, entry.name));
|
|
1686
|
+
if (platform)
|
|
1687
|
+
found.push({ platform, dir: entry.name });
|
|
1688
|
+
}
|
|
1689
|
+
return found;
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Decides how this run gets to ONE schema — before anything is downloaded or
|
|
1693
|
+
* written, because the answer can be "stop".
|
|
1694
|
+
*/
|
|
1695
|
+
async function resolveSchemaComposition(input) {
|
|
1696
|
+
const schemaPath = input.plan.canonicalSchemaPath;
|
|
1697
|
+
if (existsSync(schemaPath))
|
|
1698
|
+
return { alreadyPresent: true };
|
|
1699
|
+
// A fresh composition seeds the shared schema from the clients it is about
|
|
1700
|
+
// to scaffold; there is nothing to move and nobody to ask.
|
|
1701
|
+
if (input.plan.mode !== "add-client")
|
|
1702
|
+
return { alreadyPresent: false };
|
|
1703
|
+
const root = input.plan.projectRoot;
|
|
1704
|
+
const existing = ["web", "ios"]
|
|
1705
|
+
.map((platform) => ({ platform, schemaPath: findClientSchema(root, platform) }))
|
|
1706
|
+
.find((candidate) => candidate.schemaPath !== null);
|
|
1707
|
+
if (existing) {
|
|
1708
|
+
// The flat single-client layout `primitive init` still produces: the client
|
|
1709
|
+
// IS the repository root, and its schema has to become the app's.
|
|
1710
|
+
const from = relative(root, existing.schemaPath);
|
|
1711
|
+
// Moving the schema is only half of it — what reads the schema has to be
|
|
1712
|
+
// able to name the new location. A client that cannot be rewired would keep
|
|
1713
|
+
// its own copy beside the app's, which is the drift the one schema exists
|
|
1714
|
+
// to prevent, so the run stops here: before the download, before the move,
|
|
1715
|
+
// with the repository exactly as it was.
|
|
1716
|
+
const blocked = checkClientWiring(root, existing.platform, schemaPath);
|
|
1717
|
+
if (blocked) {
|
|
1718
|
+
error(`The ${platformLabel(existing.platform)} client at ${root} cannot be pointed at ${relative(root, schemaPath)}: ${blocked}.`);
|
|
1719
|
+
error(`Every client of an app reads one schema, so ${from} stays where it is. Restore that file to its template shape, or move the schema and rewire the client by hand, then re-run this command.`);
|
|
1720
|
+
warn("No project files were written.");
|
|
1721
|
+
process.exit(1);
|
|
1722
|
+
}
|
|
1723
|
+
info(`This repository's model schema is at ${from}. Adding a second client means moving it to models/models.toml so both clients read one copy.`);
|
|
1724
|
+
if (!input.promoteConsent) {
|
|
1725
|
+
if (!input.interactive) {
|
|
1726
|
+
error(`Adding a client needs the schema at models/models.toml, and moving ${from} there needs your consent.`);
|
|
1727
|
+
error(input.nonInteractive
|
|
1728
|
+
? 'Set `promote_schema = true` alongside `action = "add-client"` in the init config to allow the move.'
|
|
1729
|
+
: "Re-run from an interactive terminal, or with an init config setting `promote_schema = true`, to allow the move.");
|
|
1730
|
+
warn("No project files were written.");
|
|
1731
|
+
process.exit(1);
|
|
1732
|
+
}
|
|
1733
|
+
const proceed = await confirm({
|
|
1734
|
+
message: `Move ${from} to models/models.toml and point both clients at it?`,
|
|
1735
|
+
default: true,
|
|
1736
|
+
});
|
|
1737
|
+
if (!proceed) {
|
|
1738
|
+
error("Aborted. No project files were written.");
|
|
1739
|
+
process.exit(1);
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
return {
|
|
1743
|
+
alreadyPresent: false,
|
|
1744
|
+
promoteFrom: { dir: root, platform: existing.platform, schemaPath: existing.schemaPath },
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
// A root that looks like a client but keeps its schema somewhere neither
|
|
1748
|
+
// template does: say exactly what to do, and write nothing.
|
|
1749
|
+
const looksLikeClient = existsSync(join(root, "package.json")) || existsSync(join(root, "Package.swift"));
|
|
1750
|
+
if (looksLikeClient) {
|
|
1751
|
+
error(`The project at ${root} has a client, but no model schema where either template keeps one (src/models/models.toml, or Sources/<App>/Models/models.toml).`);
|
|
1752
|
+
error(`Move that client's schema to ${root}/models/models.toml and point its codegen at it, then re-run this command.`);
|
|
1753
|
+
warn("No project files were written.");
|
|
1754
|
+
process.exit(1);
|
|
1755
|
+
}
|
|
1756
|
+
return { alreadyPresent: false };
|
|
1757
|
+
}
|
|
1758
|
+
/**
|
|
1759
|
+
* Writes the app's one schema and points every client at it.
|
|
1760
|
+
*
|
|
1761
|
+
* The promoted file and the new wiring are written BEFORE the old client-local
|
|
1762
|
+
* copy goes away, so an interrupted promotion leaves a client with a schema
|
|
1763
|
+
* rather than one with neither.
|
|
1764
|
+
*/
|
|
1765
|
+
function composeSchema(input) {
|
|
1766
|
+
const { plan, schemaPlan } = input;
|
|
1767
|
+
const schemaPath = plan.canonicalSchemaPath;
|
|
1768
|
+
const reportAddedModels = (added) => {
|
|
1769
|
+
if (added.length === 0)
|
|
1770
|
+
return;
|
|
1771
|
+
info(`Added ${added.map((model) => `\`${model}\``).join(", ")} to ${relative(plan.projectRoot, schemaPath)} — the models the added client's own code reads. Models the app already declares are untouched.`);
|
|
1772
|
+
};
|
|
1773
|
+
const clientsToWire = [
|
|
1774
|
+
...(schemaPlan.promoteFrom
|
|
1775
|
+
? [{ platform: schemaPlan.promoteFrom.platform, dir: schemaPlan.promoteFrom.dir }]
|
|
1776
|
+
: []),
|
|
1777
|
+
...plan.clients,
|
|
1778
|
+
];
|
|
1779
|
+
// Asked of every client before the shared schema exists. A client that cannot
|
|
1780
|
+
// be pointed at it keeps its own copy — so writing the shared one anyway, and
|
|
1781
|
+
// wiring whichever clients did work, would leave the app with two schemas and
|
|
1782
|
+
// the run reporting success. One schema, or none: the existing client's
|
|
1783
|
+
// shape was already checked before the download, so this catches an installed
|
|
1784
|
+
// template whose wiring this CLI no longer recognizes.
|
|
1785
|
+
const unreachable = clientsToWire
|
|
1786
|
+
.map((client) => ({
|
|
1787
|
+
client,
|
|
1788
|
+
reason: checkClientWiring(client.dir, client.platform, schemaPath),
|
|
1789
|
+
}))
|
|
1790
|
+
.filter((checked) => checked.reason !== null);
|
|
1791
|
+
if (unreachable.length > 0) {
|
|
1792
|
+
for (const { client, reason } of unreachable) {
|
|
1793
|
+
error(`The ${platformLabel(client.platform)} client at ${client.dir} cannot be pointed at ${schemaPath}: ${reason}.`);
|
|
1794
|
+
}
|
|
1795
|
+
error("Every client of an app reads one model schema, so nothing was moved and no client was rewired.");
|
|
1796
|
+
warn(`The templates this run installed are still in place; remove them and re-run, or point each client at ${schemaPath} by hand.`);
|
|
1797
|
+
process.exit(1);
|
|
1798
|
+
}
|
|
1799
|
+
// Every client this run installs ships a starter schema AND code generated
|
|
1800
|
+
// from it — the web template's store reads `UserPref`, and its generator
|
|
1801
|
+
// refuses a schema with no models at all. So the app's schema has to declare
|
|
1802
|
+
// what each client reads, whether it is being written now or was already
|
|
1803
|
+
// there: a promoted comment-only Swift starter would otherwise hand a new web
|
|
1804
|
+
// client a schema it cannot generate from, and the run would report success.
|
|
1805
|
+
const starters = plan.clients
|
|
1806
|
+
.map((client) => findClientSchema(client.dir, client.platform))
|
|
1807
|
+
.filter((found) => found !== null)
|
|
1808
|
+
.map((found) => readFileSync(found, "utf-8"));
|
|
1809
|
+
if (!schemaPlan.alreadyPresent) {
|
|
1810
|
+
// The base is the promoted client's schema when one is being promoted —
|
|
1811
|
+
// its models are the app's — else the richest starter this run installs.
|
|
1812
|
+
const base = (schemaPlan.promoteFrom
|
|
1813
|
+
? readFileSync(schemaPlan.promoteFrom.schemaPath, "utf-8")
|
|
1814
|
+
: starters.find((starter) => declaredModelNames(starter).length > 0)) ??
|
|
1815
|
+
starters[0] ??
|
|
1816
|
+
FALLBACK_STARTER_SCHEMA;
|
|
1817
|
+
const composed = composeSchemaContent({ base, starters });
|
|
1818
|
+
if (writeCanonicalSchema(schemaPath, `${CANONICAL_SCHEMA_HEADER}\n${composed.content}`)) {
|
|
1819
|
+
success(`Wrote ${relative(plan.projectRoot, schemaPath)} — the app's one model schema.`);
|
|
1820
|
+
reportAddedModels(schemaPlan.promoteFrom ? composed.added : []);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
else {
|
|
1824
|
+
const composed = composeSchemaContent({
|
|
1825
|
+
base: readFileSync(schemaPath, "utf-8"),
|
|
1826
|
+
starters,
|
|
1827
|
+
});
|
|
1828
|
+
if (composed.added.length > 0) {
|
|
1829
|
+
writeFileSync(schemaPath, composed.content);
|
|
1830
|
+
reportAddedModels(composed.added);
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
for (const client of clientsToWire) {
|
|
1834
|
+
const result = wireClientToSchema(client.dir, client.platform, schemaPath);
|
|
1835
|
+
if (result.error) {
|
|
1836
|
+
// The check above said this client could be wired, so an error here is
|
|
1837
|
+
// the filesystem changing under the run — still not something to finish
|
|
1838
|
+
// on top of, because it is a client reading a schema of its own.
|
|
1839
|
+
error(`Could not point the ${platformLabel(client.platform)} client at the shared schema: ${result.error}.`);
|
|
1840
|
+
error(`Point it at ${schemaPath} before using this repository.`);
|
|
1841
|
+
process.exit(1);
|
|
1842
|
+
}
|
|
1843
|
+
success(`${platformLabel(client.platform)} client reads ${relative(client.dir, schemaPath)}.`);
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
361
1846
|
//# sourceMappingURL=init.js.map
|