primitive-admin 1.1.0-alpha.4 → 1.1.0-alpha.41

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.
Files changed (219) hide show
  1. package/README.md +325 -31
  2. package/assets/skill/skills/primitive-platform/SKILL.md +281 -0
  3. package/dist/bin/primitive.d.ts +2 -0
  4. package/dist/bin/primitive.js +268 -16
  5. package/dist/bin/primitive.js.map +1 -1
  6. package/dist/src/commands/admins.d.ts +2 -0
  7. package/dist/src/commands/admins.js +123 -18
  8. package/dist/src/commands/admins.js.map +1 -1
  9. package/dist/src/commands/analytics.d.ts +2 -0
  10. package/dist/src/commands/analytics.js +464 -55
  11. package/dist/src/commands/analytics.js.map +1 -1
  12. package/dist/src/commands/apps.d.ts +2 -0
  13. package/dist/src/commands/apps.js +55 -13
  14. package/dist/src/commands/apps.js.map +1 -1
  15. package/dist/src/commands/auth.d.ts +2 -0
  16. package/dist/src/commands/auth.js +177 -7
  17. package/dist/src/commands/auth.js.map +1 -1
  18. package/dist/src/commands/blob-buckets.d.ts +2 -0
  19. package/dist/src/commands/blob-buckets.js +422 -0
  20. package/dist/src/commands/blob-buckets.js.map +1 -0
  21. package/dist/src/commands/catalog.d.ts +2 -0
  22. package/dist/src/commands/catalog.js +34 -35
  23. package/dist/src/commands/catalog.js.map +1 -1
  24. package/dist/src/commands/collection-type-configs.d.ts +2 -0
  25. package/dist/src/commands/collection-type-configs.js +178 -0
  26. package/dist/src/commands/collection-type-configs.js.map +1 -0
  27. package/dist/src/commands/collections.d.ts +2 -0
  28. package/dist/src/commands/collections.js +515 -0
  29. package/dist/src/commands/collections.js.map +1 -0
  30. package/dist/src/commands/comparisons.d.ts +2 -0
  31. package/dist/src/commands/comparisons.js +6 -6
  32. package/dist/src/commands/comparisons.js.map +1 -1
  33. package/dist/src/commands/cron-triggers.d.ts +2 -0
  34. package/dist/src/commands/cron-triggers.js +357 -0
  35. package/dist/src/commands/cron-triggers.js.map +1 -0
  36. package/dist/src/commands/database-types.d.ts +2 -0
  37. package/dist/src/commands/database-types.js +471 -0
  38. package/dist/src/commands/database-types.js.map +1 -0
  39. package/dist/src/commands/databases.d.ts +2 -0
  40. package/dist/src/commands/databases.js +1462 -83
  41. package/dist/src/commands/databases.js.map +1 -1
  42. package/dist/src/commands/documents.d.ts +2 -0
  43. package/dist/src/commands/documents.js +518 -11
  44. package/dist/src/commands/documents.js.map +1 -1
  45. package/dist/src/commands/email-templates.d.ts +2 -0
  46. package/dist/src/commands/email-templates.js +281 -0
  47. package/dist/src/commands/email-templates.js.map +1 -0
  48. package/dist/src/commands/env.d.ts +12 -0
  49. package/dist/src/commands/env.js +260 -0
  50. package/dist/src/commands/env.js.map +1 -0
  51. package/dist/src/commands/group-type-configs.d.ts +2 -0
  52. package/dist/src/commands/group-type-configs.js +181 -0
  53. package/dist/src/commands/group-type-configs.js.map +1 -0
  54. package/dist/src/commands/groups.d.ts +2 -0
  55. package/dist/src/commands/groups.js +48 -45
  56. package/dist/src/commands/groups.js.map +1 -1
  57. package/dist/src/commands/guides.d.ts +159 -0
  58. package/dist/src/commands/guides.js +449 -35
  59. package/dist/src/commands/guides.js.map +1 -1
  60. package/dist/src/commands/init.d.ts +17 -0
  61. package/dist/src/commands/init.js +811 -201
  62. package/dist/src/commands/init.js.map +1 -1
  63. package/dist/src/commands/integrations.d.ts +2 -0
  64. package/dist/src/commands/integrations.js +533 -46
  65. package/dist/src/commands/integrations.js.map +1 -1
  66. package/dist/src/commands/llm.d.ts +2 -0
  67. package/dist/src/commands/llm.js +4 -2
  68. package/dist/src/commands/llm.js.map +1 -1
  69. package/dist/src/commands/metadata.d.ts +2 -0
  70. package/dist/src/commands/metadata.js +194 -0
  71. package/dist/src/commands/metadata.js.map +1 -0
  72. package/dist/src/commands/prompts.d.ts +2 -0
  73. package/dist/src/commands/prompts.js +50 -52
  74. package/dist/src/commands/prompts.js.map +1 -1
  75. package/dist/src/commands/rule-sets.d.ts +3 -0
  76. package/dist/src/commands/rule-sets.js +388 -0
  77. package/dist/src/commands/rule-sets.js.map +1 -0
  78. package/dist/src/commands/scripts.d.ts +2 -0
  79. package/dist/src/commands/scripts.js +667 -0
  80. package/dist/src/commands/scripts.js.map +1 -0
  81. package/dist/src/commands/secrets.d.ts +2 -0
  82. package/dist/src/commands/secrets.js +108 -0
  83. package/dist/src/commands/secrets.js.map +1 -0
  84. package/dist/src/commands/skill.d.ts +2 -0
  85. package/dist/src/commands/skill.js +29 -0
  86. package/dist/src/commands/skill.js.map +1 -0
  87. package/dist/src/commands/sync.d.ts +175 -0
  88. package/dist/src/commands/sync.js +4412 -233
  89. package/dist/src/commands/sync.js.map +1 -1
  90. package/dist/src/commands/tokens.d.ts +2 -0
  91. package/dist/src/commands/tokens.js +122 -19
  92. package/dist/src/commands/tokens.js.map +1 -1
  93. package/dist/src/commands/users.d.ts +2 -0
  94. package/dist/src/commands/users.js +440 -22
  95. package/dist/src/commands/users.js.map +1 -1
  96. package/dist/src/commands/waitlist.d.ts +2 -0
  97. package/dist/src/commands/waitlist.js +10 -10
  98. package/dist/src/commands/waitlist.js.map +1 -1
  99. package/dist/src/commands/webhooks.d.ts +2 -0
  100. package/dist/src/commands/webhooks.js +391 -0
  101. package/dist/src/commands/webhooks.js.map +1 -0
  102. package/dist/src/commands/workflows.d.ts +49 -0
  103. package/dist/src/commands/workflows.js +1009 -123
  104. package/dist/src/commands/workflows.js.map +1 -1
  105. package/dist/src/lib/api-client.d.ts +1283 -0
  106. package/dist/src/lib/api-client.js +1129 -83
  107. package/dist/src/lib/api-client.js.map +1 -1
  108. package/dist/src/lib/auth-flow.d.ts +8 -0
  109. package/dist/src/lib/block-layout.d.ts +160 -0
  110. package/dist/src/lib/block-layout.js +451 -0
  111. package/dist/src/lib/block-layout.js.map +1 -0
  112. package/dist/src/lib/cli-manifest.d.ts +60 -0
  113. package/dist/src/lib/cli-manifest.js +70 -0
  114. package/dist/src/lib/cli-manifest.js.map +1 -0
  115. package/dist/src/lib/config.d.ts +53 -0
  116. package/dist/src/lib/config.js +92 -53
  117. package/dist/src/lib/config.js.map +1 -1
  118. package/dist/src/lib/confirm-prompt.d.ts +83 -0
  119. package/dist/src/lib/confirm-prompt.js +110 -0
  120. package/dist/src/lib/confirm-prompt.js.map +1 -0
  121. package/dist/src/lib/constants.d.ts +2 -0
  122. package/dist/src/lib/constants.js +3 -0
  123. package/dist/src/lib/constants.js.map +1 -0
  124. package/dist/src/lib/crash-handlers.d.ts +20 -0
  125. package/dist/src/lib/crash-handlers.js +49 -0
  126. package/dist/src/lib/crash-handlers.js.map +1 -0
  127. package/dist/src/lib/credentials-store.d.ts +79 -0
  128. package/dist/src/lib/credentials-store.js +307 -0
  129. package/dist/src/lib/credentials-store.js.map +1 -0
  130. package/dist/src/lib/csv.d.ts +48 -0
  131. package/dist/src/lib/csv.js +177 -0
  132. package/dist/src/lib/csv.js.map +1 -0
  133. package/dist/src/lib/db-codegen/dbFingerprint.d.ts +10 -0
  134. package/dist/src/lib/db-codegen/dbFingerprint.js +17 -0
  135. package/dist/src/lib/db-codegen/dbFingerprint.js.map +1 -0
  136. package/dist/src/lib/db-codegen/dbGenerator.d.ts +111 -0
  137. package/dist/src/lib/db-codegen/dbGenerator.js +371 -0
  138. package/dist/src/lib/db-codegen/dbGenerator.js.map +1 -0
  139. package/dist/src/lib/db-codegen/dbNaming.d.ts +45 -0
  140. package/dist/src/lib/db-codegen/dbNaming.js +104 -0
  141. package/dist/src/lib/db-codegen/dbNaming.js.map +1 -0
  142. package/dist/src/lib/db-codegen/dbTemplates.d.ts +97 -0
  143. package/dist/src/lib/db-codegen/dbTemplates.js +159 -0
  144. package/dist/src/lib/db-codegen/dbTemplates.js.map +1 -0
  145. package/dist/src/lib/db-codegen/dbTsTypes.d.ts +78 -0
  146. package/dist/src/lib/db-codegen/dbTsTypes.js +112 -0
  147. package/dist/src/lib/db-codegen/dbTsTypes.js.map +1 -0
  148. package/dist/src/lib/env-resolver.d.ts +62 -0
  149. package/dist/src/lib/env-resolver.js +121 -0
  150. package/dist/src/lib/env-resolver.js.map +1 -0
  151. package/dist/src/lib/fetch.d.ts +5 -0
  152. package/dist/src/lib/generated-allowlist.d.ts +28 -0
  153. package/dist/src/lib/generated-allowlist.js +220 -0
  154. package/dist/src/lib/generated-allowlist.js.map +1 -0
  155. package/dist/src/lib/init-config.d.ts +59 -0
  156. package/dist/src/lib/init-config.js +113 -0
  157. package/dist/src/lib/init-config.js.map +1 -0
  158. package/dist/src/lib/migration-nag.d.ts +49 -0
  159. package/dist/src/lib/migration-nag.js +163 -0
  160. package/dist/src/lib/migration-nag.js.map +1 -0
  161. package/dist/src/lib/output.d.ts +86 -0
  162. package/dist/src/lib/output.js +150 -8
  163. package/dist/src/lib/output.js.map +1 -1
  164. package/dist/src/lib/paginate.d.ts +33 -0
  165. package/dist/src/lib/paginate.js +42 -0
  166. package/dist/src/lib/paginate.js.map +1 -0
  167. package/dist/src/lib/project-config.d.ts +97 -0
  168. package/dist/src/lib/project-config.js +209 -0
  169. package/dist/src/lib/project-config.js.map +1 -0
  170. package/dist/src/lib/query-operators.d.ts +43 -0
  171. package/dist/src/lib/query-operators.js +80 -0
  172. package/dist/src/lib/query-operators.js.map +1 -0
  173. package/dist/src/lib/refresh-admin-credentials.d.ts +65 -0
  174. package/dist/src/lib/refresh-admin-credentials.js +103 -0
  175. package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
  176. package/dist/src/lib/resolve-platform.d.ts +45 -0
  177. package/dist/src/lib/resolve-platform.js +43 -0
  178. package/dist/src/lib/resolve-platform.js.map +1 -0
  179. package/dist/src/lib/skill-installer.d.ts +23 -0
  180. package/dist/src/lib/skill-installer.js +135 -0
  181. package/dist/src/lib/skill-installer.js.map +1 -0
  182. package/dist/src/lib/snapshots.d.ts +99 -0
  183. package/dist/src/lib/snapshots.js +357 -0
  184. package/dist/src/lib/snapshots.js.map +1 -0
  185. package/dist/src/lib/sync-paths.d.ts +72 -0
  186. package/dist/src/lib/sync-paths.js +130 -0
  187. package/dist/src/lib/sync-paths.js.map +1 -0
  188. package/dist/src/lib/template.d.ts +97 -0
  189. package/dist/src/lib/template.js +336 -62
  190. package/dist/src/lib/template.js.map +1 -1
  191. package/dist/src/lib/token-inject.d.ts +56 -0
  192. package/dist/src/lib/token-inject.js +204 -0
  193. package/dist/src/lib/token-inject.js.map +1 -0
  194. package/dist/src/lib/toml-database-config.d.ts +152 -0
  195. package/dist/src/lib/toml-database-config.js +600 -0
  196. package/dist/src/lib/toml-database-config.js.map +1 -0
  197. package/dist/src/lib/toml-metadata-config.d.ts +139 -0
  198. package/dist/src/lib/toml-metadata-config.js +427 -0
  199. package/dist/src/lib/toml-metadata-config.js.map +1 -0
  200. package/dist/src/lib/toml-params-validator.d.ts +129 -0
  201. package/dist/src/lib/toml-params-validator.js +298 -0
  202. package/dist/src/lib/toml-params-validator.js.map +1 -0
  203. package/dist/src/lib/version-check.d.ts +10 -0
  204. package/dist/src/lib/version-check.js +172 -0
  205. package/dist/src/lib/version-check.js.map +1 -0
  206. package/dist/src/lib/workflow-apply.d.ts +66 -0
  207. package/dist/src/lib/workflow-apply.js +117 -0
  208. package/dist/src/lib/workflow-apply.js.map +1 -0
  209. package/dist/src/lib/workflow-fragments.d.ts +41 -0
  210. package/dist/src/lib/workflow-fragments.js +121 -0
  211. package/dist/src/lib/workflow-fragments.js.map +1 -0
  212. package/dist/src/lib/workflow-toml-validator.d.ts +95 -0
  213. package/dist/src/lib/workflow-toml-validator.js +323 -0
  214. package/dist/src/lib/workflow-toml-validator.js.map +1 -0
  215. package/dist/src/types/index.d.ts +520 -0
  216. package/dist/src/validators.d.ts +64 -0
  217. package/dist/src/validators.js +63 -0
  218. package/dist/src/validators.js.map +1 -0
  219. package/package.json +18 -4
@@ -1,107 +1,480 @@
1
- import { existsSync } from "fs";
2
- import { resolve, basename } from "path";
3
- import { input, confirm, select } from "@inquirer/prompts";
4
- import { loadCredentials } from "../lib/config.js";
5
- import { browserLogin } from "../lib/auth-flow.js";
1
+ import { confirm, input, select } from "@inquirer/prompts";
2
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from "fs";
3
+ import { basename, join, resolve } from "path";
4
+ import { CURRENT_CONFIG_VERSION, PROJECT_CONFIG_DIR, PROJECT_CONFIG_FILENAME, PROJECT_CONFIG_DISPLAY_NAME, saveProjectConfig, loadProjectConfigFromPath, } from "../lib/project-config.js";
6
5
  import { ApiClient } from "../lib/api-client.js";
7
- import { downloadAndExtractTemplate, updateEnvFile, runPnpmInstall, isDirectoryEmpty, isPnpmInstalled, installPnpmGlobally, isPrimitiveCliInstalled, installPrimitiveCli, } from "../lib/template.js";
8
- import { success, error, info, warn, keyValue, divider, } from "../lib/output.js";
6
+ import { browserLogin } from "../lib/auth-flow.js";
7
+ import { loadCredentials, setCurrentApp } from "../lib/config.js";
8
+ import { INIT_CONFIG_FILENAME, loadInitConfig, parseInitConfigToml, resolveUseExistingAppId, validateInitConfig, } from "../lib/init-config.js";
9
+ import { resolvePlatform, } from "../lib/resolve-platform.js";
10
+ import { divider, error, info, keyValue, success, warn, } from "../lib/output.js";
11
+ import { gitInitialCommit, gitInitRepo, installPnpmGlobally, installPrimitiveCli, isDirectoryEmpty, isPnpmInstalled, isPortFree, isPrimitiveCliInstalled, isXcodeInstalled, IOS_TEMPLATE_URL, prepareTemplate, runPnpmInstall, runSwiftPackageResolve, updateEnvFile, updateEnvPort, updatePrimitiveJson, updateViteConfig, } from "../lib/template.js";
9
12
  const DEFAULT_SERVER_URL = process.env.PRIMITIVE_SERVER_URL || "https://primitiveapi.com";
13
+ /**
14
+ * The .gitignore lines `init` writes for Primitive CLI local state.
15
+ *
16
+ * Pre-pull sync snapshots (issue #578) are per-machine local recovery state and
17
+ * must never be committed. They can land in three places depending on how the
18
+ * sync dir resolves (see `resolveSnapshotsRoot` in `lib/sync-paths.ts`):
19
+ * - project mode: `<projectRoot>/.primitive/sync-backups/<env>/<appId>/`
20
+ * - legacy `--dir <p>`: `<p>/.snapshots/` (sibling of the user dir)
21
+ * - no-project fallback: `./config-snapshots/`
22
+ * The bare `.snapshots/` and `config-snapshots/` entries (no leading slash)
23
+ * match those directories wherever `--dir` points, so backup copies — including
24
+ * the restored `.primitive-sync.json` baseline — can never surface as untracked
25
+ * files in a tracked custom config dir.
26
+ */
27
+ export const GITIGNORE_LOCAL_STATE_ENTRIES = [
28
+ ".primitive/credentials.json",
29
+ ".primitive/sync/**/.primitive-sync.json",
30
+ ".primitive/sync-backups/",
31
+ ".snapshots/",
32
+ "config-snapshots/",
33
+ ];
34
+ /**
35
+ * Writes .primitive/config.json into the project directory and ensures the
36
+ * .gitignore entry for local state exists. Safe to call in a directory that
37
+ * already has a config — in that case we add/update the "dev" environment
38
+ * instead of overwriting the file.
39
+ */
40
+ async function scaffoldProjectConfig(opts) {
41
+ const configPath = join(opts.targetDir, PROJECT_CONFIG_DIR, PROJECT_CONFIG_FILENAME);
42
+ let config;
43
+ if (existsSync(configPath)) {
44
+ // Respect an existing config: merge in the new dev env if missing.
45
+ try {
46
+ config = loadProjectConfigFromPath(configPath);
47
+ }
48
+ catch {
49
+ // Existing file is broken — don't clobber it; just warn and skip.
50
+ warn(`${PROJECT_CONFIG_DISPLAY_NAME} exists but is invalid. Skipping project config scaffold.`);
51
+ return;
52
+ }
53
+ if (!config.environments.dev) {
54
+ config.environments.dev = {
55
+ apiUrl: opts.serverUrl,
56
+ appId: opts.appId,
57
+ appName: opts.appName,
58
+ };
59
+ if (!config.defaultEnvironment)
60
+ config.defaultEnvironment = "dev";
61
+ }
62
+ }
63
+ else {
64
+ config = {
65
+ version: CURRENT_CONFIG_VERSION,
66
+ defaultEnvironment: "dev",
67
+ environments: {
68
+ dev: {
69
+ apiUrl: opts.serverUrl,
70
+ appId: opts.appId,
71
+ appName: opts.appName,
72
+ },
73
+ },
74
+ };
75
+ }
76
+ saveProjectConfig(configPath, config);
77
+ // Ensure .primitive/ local state (credentials, per-machine sync state) is
78
+ // gitignored. The config.json itself lives in .primitive/ too, but we
79
+ // gitignore only the sensitive / per-machine artifacts — config.json and
80
+ // the exported TOML files under .primitive/sync/<env>/<appId>/ are meant
81
+ // to be committed. Only `.primitive-sync.json` (the auto-managed sync
82
+ // state baseline) is per-machine local state; everything else in the
83
+ // sync directory is the version-controllable export.
84
+ //
85
+ // Legacy note: an earlier version of init wrote `.primitive/sync/` here,
86
+ // which incorrectly hid the TOML export tree from `git status`. If we see
87
+ // that legacy entry, replace it with the targeted pattern in-place so
88
+ // users who re-run init pick up the fix.
89
+ const gitignorePath = join(opts.targetDir, ".gitignore");
90
+ const LEGACY_SYNC_IGNORE = ".primitive/sync/";
91
+ const gitignoreEntries = [...GITIGNORE_LOCAL_STATE_ENTRIES];
92
+ let linesToAdd = [];
93
+ if (existsSync(gitignorePath)) {
94
+ const existing = readFileSync(gitignorePath, "utf-8");
95
+ const trimmedLines = existing.split(/\r?\n/).map((l) => l.trim());
96
+ // Migrate the legacy `.primitive/sync/` entry to the targeted pattern,
97
+ // if present. Preserve original line endings/formatting elsewhere.
98
+ if (trimmedLines.includes(LEGACY_SYNC_IGNORE)) {
99
+ const rewritten = existing
100
+ .split(/\r?\n/)
101
+ .map((line) => line.trim() === LEGACY_SYNC_IGNORE
102
+ ? ".primitive/sync/**/.primitive-sync.json"
103
+ : line)
104
+ .join("\n");
105
+ try {
106
+ writeFileSync(gitignorePath, rewritten);
107
+ }
108
+ catch {
109
+ // Non-fatal
110
+ }
111
+ }
112
+ // Re-read after potential migration so we don't double-add entries.
113
+ const refreshed = readFileSync(gitignorePath, "utf-8")
114
+ .split(/\r?\n/)
115
+ .map((l) => l.trim());
116
+ for (const entry of gitignoreEntries) {
117
+ if (!refreshed.includes(entry)) {
118
+ linesToAdd.push(entry);
119
+ }
120
+ }
121
+ }
122
+ else {
123
+ linesToAdd = [...gitignoreEntries];
124
+ }
125
+ if (linesToAdd.length > 0) {
126
+ try {
127
+ const prefix = existsSync(gitignorePath) ? "\n# Primitive CLI local state\n" : "# Primitive CLI local state\n";
128
+ appendFileSync(gitignorePath, prefix + linesToAdd.join("\n") + "\n");
129
+ }
130
+ catch {
131
+ // Non-fatal
132
+ }
133
+ }
134
+ success(`Wrote ${PROJECT_CONFIG_DISPLAY_NAME} with dev environment`);
135
+ }
136
+ function validateServerUrl(url) {
137
+ if (!url.trim())
138
+ return "Server URL cannot be empty";
139
+ try {
140
+ new URL(url);
141
+ return true;
142
+ }
143
+ catch {
144
+ return "Please enter a valid URL (e.g., https://example.com)";
145
+ }
146
+ }
147
+ /**
148
+ * Prompts for login when credentials are missing or the server has changed.
149
+ * Returns updated credentials on success, or exits/warns on failure.
150
+ * In non-interactive mode, auto-proceeds with login (no confirmation prompt).
151
+ */
152
+ async function ensureAuthenticated(credentials, serverUrl, serverChanged, context) {
153
+ if (credentials && !serverChanged)
154
+ return credentials;
155
+ if (serverChanged) {
156
+ info(`Switching to ${serverUrl}. You need to log in.`);
157
+ }
158
+ else {
159
+ info(`You need to log in to ${context.reason}.`);
160
+ }
161
+ if (context.nonInteractive && context.allowSkip) {
162
+ warn(`Skipping ${context.reason}; no authentication is available.`);
163
+ warn(`Make sure the app ID is correct.`);
164
+ return null;
165
+ }
166
+ let shouldLogin;
167
+ if (context.nonInteractive) {
168
+ // In non-interactive mode, auto-proceed with login
169
+ shouldLogin = true;
170
+ info("Attempting automatic login...");
171
+ }
172
+ else {
173
+ shouldLogin = await confirm({
174
+ message: "Log in now?",
175
+ default: true,
176
+ });
177
+ }
178
+ if (!shouldLogin) {
179
+ if (!context.allowSkip) {
180
+ error(`Cannot ${context.reason} without authentication. Use --use-existing-app-id to skip app creation.`);
181
+ // Authentication happens before template prep, so a bail-out here leaves
182
+ // nothing on disk.
183
+ warn(`No project files were written. Re-run \`primitive init\` once you can authenticate.`);
184
+ process.exit(1);
185
+ }
186
+ warn(`Skipping. Make sure the app ID is correct.`);
187
+ return null;
188
+ }
189
+ const result = await browserLogin(serverUrl);
190
+ if (!result.success) {
191
+ error(`Login failed: ${result.error}`);
192
+ warn(`No project files were written. Re-run \`primitive init\` once you can authenticate.`);
193
+ process.exit(1);
194
+ }
195
+ success(`Logged in as ${result.credentials.email}`);
196
+ return result.credentials;
197
+ }
198
+ /**
199
+ * Loads the init config from --config flag or auto-discovers it in CWD.
200
+ * Returns null if no config file is found/specified.
201
+ * Exits with error if config file is specified but invalid.
202
+ */
203
+ function loadInitConfigFromOptions(options) {
204
+ let config = null;
205
+ if (options.config) {
206
+ // Explicit config path
207
+ const configPath = resolve(process.cwd(), options.config);
208
+ if (!existsSync(configPath)) {
209
+ error(`Config file not found: ${configPath}`);
210
+ process.exit(1);
211
+ }
212
+ try {
213
+ const content = readFileSync(configPath, "utf-8");
214
+ config = parseInitConfigToml(content);
215
+ }
216
+ catch (err) {
217
+ error(`Failed to parse config file: ${err.message}`);
218
+ process.exit(1);
219
+ }
220
+ }
221
+ else {
222
+ // Auto-discover in CWD
223
+ config = loadInitConfig(process.cwd());
224
+ }
225
+ if (!config)
226
+ return null;
227
+ // Validate
228
+ info(`Using config file for non-interactive init`);
229
+ const validation = validateInitConfig(config);
230
+ if (!validation.valid) {
231
+ error("Invalid config file:");
232
+ for (const err of validation.errors) {
233
+ error(` - ${err}`);
234
+ }
235
+ process.exit(1);
236
+ }
237
+ return config;
238
+ }
10
239
  export function registerInitCommand(program) {
11
240
  program
12
241
  .command("init")
13
242
  .description("Create a new Primitive app from template")
14
243
  .argument("[directory]", "Directory to create the project in (default: current directory)")
15
244
  .option("--use-existing-app-id <id>", "Use an existing app ID instead of creating a new one")
16
- .option("-s, --server <url>", "Server URL", DEFAULT_SERVER_URL)
17
- .option("--skip-install", "Skip running pnpm install")
245
+ .option("-s, --server <url>", "Server URL (default: current CLI server or https://primitiveapi.com)")
246
+ .option("--skip-install", "Skip dependency installation")
247
+ .option("--config <path>", `Path to an init config file for non-interactive setup (default: auto-discover ${INIT_CONFIG_FILENAME} in the current directory)`)
248
+ .option("-p, --platform <platform>", "Target platform: web or ios (SwiftUI). Omit to be prompted interactively.")
18
249
  .addHelpText("after", `
19
250
  Examples:
20
- $ primitive init my-app # Create in ./my-app, prompt for app name
21
- $ primitive init # Interactive: prompt for directory and app name
251
+ $ primitive init my-app # Create web app in ./my-app
252
+ $ primitive init # Interactive: prompt for directory and app name
253
+ $ primitive init my-app --platform ios # Create an iOS (SwiftUI) app
22
254
  $ primitive init . --use-existing-app-id 01HXY... # Use existing app in current dir
23
255
  $ primitive init my-app --skip-install # Skip dependency install
256
+ $ primitive init --config .primitive-init.toml # Non-interactive from config file
257
+
258
+ Non-interactive mode:
259
+ Create a ${INIT_CONFIG_FILENAME} file (or pass --config <path>) to skip all prompts.
260
+ The config file uses TOML format:
261
+
262
+ action = "create" # "create" or "use-existing"
263
+ app_name = "My App" # Required for action = "create"
264
+ app_id = "01HXY..." # Required for action = "use-existing"
265
+ dir = "my-app" # Project directory (optional)
266
+ platform = "web" # "web" or "ios" (optional, defaults to web)
267
+ access_mode = "invite-only" # "public", "domain", or "invite-only" (optional)
268
+ dev_port = 5173 # Dev server port (optional)
269
+ skip_install = false # Skip pnpm install (optional)
270
+ server = "https://..." # Custom server URL (optional)
271
+ invite_emails = ["a@b.com"] # Emails to invite (optional, for invite-only)
272
+ allowed_domains = ["b.com"] # Allowed domains (optional, for domain mode)
273
+ overwrite = false # Overwrite non-empty target directory (optional)
24
274
 
25
275
  This command will:
26
- 1. Download the Primitive app template
276
+ 1. Download the Primitive app template (web or Apple SwiftUI)
27
277
  2. Prompt for an app name and create it on the Primitive server (or use existing)
28
278
  3. Configure the project with your app ID
29
- 4. Install dependencies with pnpm
279
+ 4. Install dependencies (pnpm for web, swift package resolve for Apple)
30
280
  `)
31
281
  .action(handleInit);
32
282
  }
33
283
  async function handleInit(projectDirArg, options) {
34
- const serverUrl = options.server || DEFAULT_SERVER_URL;
284
+ // Validate an explicitly-passed `--platform` flag up front. Only `web` and
285
+ // `ios` are supported (#1009 removed `macos`). An omitted flag is
286
+ // `undefined` (the commander default was dropped) and is resolved below.
287
+ if (options.platform !== undefined &&
288
+ options.platform !== "web" &&
289
+ options.platform !== "ios") {
290
+ error(`Unknown platform "${options.platform}". Supported platforms: web, ios`);
291
+ process.exit(1);
292
+ }
293
+ // `initConfig` is the parsed-and-validated `primitive-init.toml` for
294
+ // non-interactive runs (CI, scripted scaffolding). When non-null, the
295
+ // rest of this function uses it to skip prompts.
296
+ const initConfig = loadInitConfigFromOptions(options);
297
+ // Resolve the target platform per the 4-case interactivity contract:
298
+ // (a) explicit --platform flag -> use it (never prompt)
299
+ // (b) init-config TOML present -> use its platform or fall back to web
300
+ // (c) interactive TTY, no flag/cfg -> prompt
301
+ // (d) non-TTY, no flag/cfg -> fall back to web (no hang/crash)
302
+ // The prompt is TTY-guarded inside `resolvePlatform` so a piped/CI stdin
303
+ // never triggers the `@inquirer/prompts` `ERR_USE_AFTER_CLOSE` crash.
304
+ const platform = await resolvePlatform({
305
+ flag: options.platform,
306
+ configPlatform: initConfig?.platform,
307
+ hasConfig: initConfig != null,
308
+ isTTY: Boolean(process.stdin.isTTY),
309
+ promptFn: () => select({
310
+ message: "What platform are you building for?",
311
+ choices: [
312
+ { value: "web", name: "web (Vue 3 + TypeScript + Tailwind)" },
313
+ { value: "ios", name: "ios (Swift + SwiftUI)" },
314
+ ],
315
+ default: "web",
316
+ }),
317
+ });
318
+ // iOS shares the SwiftUI template, Xcode toolchain, and primitive.json
319
+ // config path. (macOS was removed in #1009.)
320
+ const isApple = platform === "ios";
321
+ // Apple-family platforms require macOS with Xcode.
322
+ if (isApple && process.platform !== "darwin") {
323
+ error("iOS apps require macOS with Xcode installed.");
324
+ process.exit(1);
325
+ }
326
+ if (isApple) {
327
+ const xcodeAvailable = await isXcodeInstalled();
328
+ if (!xcodeAvailable) {
329
+ error("Xcode command-line tools are required for Apple development. Install them with: xcode-select --install");
330
+ process.exit(1);
331
+ }
332
+ }
35
333
  // Step 1: Determine project directory
36
334
  let projectDir = projectDirArg;
335
+ if (!projectDir && initConfig?.dir) {
336
+ projectDir = initConfig.dir;
337
+ }
37
338
  if (!projectDir) {
38
339
  projectDir = await input({
39
- message: "Where would you like to create the project?",
40
- default: ".",
340
+ message: "Project name (a new directory will be created):",
341
+ default: "my-primitive-app",
41
342
  validate: (value) => {
42
343
  if (!value.trim())
43
- return "Directory cannot be empty";
344
+ return "Project name cannot be empty";
44
345
  return true;
45
346
  },
46
347
  });
47
348
  }
48
349
  const targetDir = resolve(process.cwd(), projectDir);
49
350
  // Used as the default app name suggestion when prompting
50
- const defaultAppName = projectDir === "." ? basename(targetDir) : projectDir;
351
+ const defaultAppName = projectDir === "." ? basename(targetDir) : basename(projectDir);
51
352
  // Step 2: Check if directory exists and is non-empty
52
353
  if (existsSync(targetDir) && !isDirectoryEmpty(targetDir)) {
53
- warn(`Directory "${projectDir}" already exists and is not empty.`);
54
- const overwrite = await confirm({
55
- message: "Do you want to overwrite the existing contents?",
56
- default: false,
57
- });
58
- if (!overwrite) {
59
- error("Aborted. Please choose a different project name.");
60
- process.exit(1);
354
+ if (initConfig) {
355
+ // Non-interactive: require explicit overwrite opt-in
356
+ if (!initConfig.overwrite) {
357
+ error(`Directory "${projectDir}" already exists and is not empty.`);
358
+ error('Set overwrite = true in your config file to allow overwriting.');
359
+ process.exit(1);
360
+ }
361
+ warn(`Directory "${projectDir}" already exists and is not empty. Overwriting (overwrite = true in config).`);
362
+ }
363
+ else {
364
+ warn(`Directory "${projectDir}" already exists and is not empty.`);
365
+ const overwrite = await confirm({
366
+ message: "Do you want to overwrite the existing contents?",
367
+ default: false,
368
+ });
369
+ if (!overwrite) {
370
+ error("Aborted. Please choose a different project name.");
371
+ process.exit(1);
372
+ }
61
373
  }
62
374
  }
63
- // Step 3: Download template
64
- info("Setting up your project...");
65
- try {
66
- await downloadAndExtractTemplate({
67
- targetDir,
68
- onProgress: info,
69
- });
375
+ // Step 2.5: Determine target server
376
+ let serverUrl;
377
+ let serverChanged = false;
378
+ if (options.server) {
379
+ const validation = validateServerUrl(options.server);
380
+ if (validation !== true) {
381
+ error(validation);
382
+ process.exit(1);
383
+ }
384
+ serverUrl = options.server;
385
+ // Check if explicit --server differs from current credentials
386
+ const existingCredentials = loadCredentials();
387
+ if (existingCredentials?.serverUrl &&
388
+ existingCredentials.serverUrl !== options.server) {
389
+ serverChanged = true;
390
+ }
70
391
  }
71
- catch (err) {
72
- error(`Failed to download template: ${err.message}`);
73
- error("Check your internet connection and try again.");
74
- process.exit(1);
392
+ else {
393
+ const existingCredentials = loadCredentials();
394
+ const currentServer = existingCredentials?.serverUrl;
395
+ if (currentServer && currentServer !== DEFAULT_SERVER_URL && !initConfig) {
396
+ console.log();
397
+ const serverChoice = await select({
398
+ message: `The Primitive CLI is currently using ${currentServer}.\nWhere do you want to create this app?`,
399
+ choices: [
400
+ {
401
+ value: currentServer,
402
+ name: `Current server (${currentServer})`,
403
+ },
404
+ {
405
+ value: DEFAULT_SERVER_URL,
406
+ name: `Production (${DEFAULT_SERVER_URL})`,
407
+ },
408
+ {
409
+ value: "__custom__",
410
+ name: "Other server",
411
+ },
412
+ ],
413
+ default: currentServer,
414
+ });
415
+ if (serverChoice === "__custom__") {
416
+ serverUrl = await input({
417
+ message: "Enter the server URL:",
418
+ validate: validateServerUrl,
419
+ });
420
+ }
421
+ else {
422
+ serverUrl = serverChoice;
423
+ }
424
+ serverChanged = currentServer !== serverUrl;
425
+ }
426
+ else {
427
+ serverUrl = currentServer || DEFAULT_SERVER_URL;
428
+ }
75
429
  }
76
430
  // Step 4: Check authentication
77
431
  let credentials = loadCredentials();
78
432
  let appId;
79
433
  let appName;
80
- if (options.useExistingAppId) {
81
- // Using existing app ID - verify it exists
82
- appId = options.useExistingAppId;
83
- appName = "(existing app)"; // Default if we can't verify
84
- if (!credentials) {
85
- info("You need to log in to verify the app.");
86
- const shouldLogin = await confirm({
87
- message: "Log in now?",
88
- default: true,
434
+ let preparedTemplate = null;
435
+ const cleanupPreparedTemplate = () => {
436
+ try {
437
+ preparedTemplate?.cleanup();
438
+ }
439
+ catch {
440
+ // Non-fatal cleanup best effort
441
+ }
442
+ preparedTemplate = null;
443
+ };
444
+ const prepareTemplateOrExit = async () => {
445
+ if (preparedTemplate)
446
+ return;
447
+ // Download and validate the archive before app creation, but keep it staged
448
+ // in a temp directory. That avoids both failure modes: app-creation failure
449
+ // leaves no scaffold, and template-download failure leaves no orphaned app.
450
+ info(`Preparing your ${platform === "ios" ? "iOS" : "web"} project template...`);
451
+ try {
452
+ preparedTemplate = await prepareTemplate({
453
+ templateUrl: isApple ? IOS_TEMPLATE_URL : undefined,
454
+ onProgress: info,
89
455
  });
90
- if (shouldLogin) {
91
- const result = await browserLogin(serverUrl);
92
- if (!result.success) {
93
- error(`Login failed: ${result.error}`);
94
- warn(`Template files were extracted to "${targetDir}" but VITE_APP_ID is not configured.`);
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}`);
100
- }
101
- else {
102
- warn("Skipping app verification. Make sure the app ID is correct.");
103
- }
104
456
  }
457
+ catch (err) {
458
+ error(`Failed to download template: ${err.message}`);
459
+ error("Check your internet connection and try again.");
460
+ process.exit(1);
461
+ }
462
+ };
463
+ // An existing app can be selected two ways: the `--use-existing-app-id` flag,
464
+ // or `action = "use-existing"` + `app_id` in the init-config TOML. Reading
465
+ // only the flag (the old behavior) made a use-existing TOML fall through to
466
+ // the create branch and prompt for an app name — hanging non-interactive
467
+ // runs. (#1217)
468
+ const useExistingAppId = resolveUseExistingAppId(options.useExistingAppId, initConfig);
469
+ if (useExistingAppId) {
470
+ // Using existing app ID - verify it exists
471
+ appId = useExistingAppId;
472
+ appName = "(existing app)"; // Default if we can't verify
473
+ credentials = await ensureAuthenticated(credentials, serverUrl, serverChanged, {
474
+ reason: "verify the app",
475
+ allowSkip: true,
476
+ nonInteractive: !!initConfig,
477
+ });
105
478
  if (credentials) {
106
479
  try {
107
480
  const client = new ApiClient();
@@ -113,36 +486,27 @@ async function handleInit(projectDirArg, options) {
113
486
  warn(`Could not verify app: ${err.message}`);
114
487
  }
115
488
  }
489
+ await prepareTemplateOrExit();
116
490
  }
117
491
  else {
118
492
  // Creating new app - authentication required
119
- if (!credentials) {
120
- info("You need to log in to create an app.");
121
- const shouldLogin = await confirm({
122
- message: "Log in now?",
123
- default: true,
493
+ credentials = await ensureAuthenticated(credentials, serverUrl, serverChanged, {
494
+ reason: "create an app",
495
+ allowSkip: false,
496
+ nonInteractive: !!initConfig,
497
+ });
498
+ // Determine app name
499
+ if (initConfig?.app_name) {
500
+ appName = initConfig.app_name;
501
+ info(`App name: ${appName} (from config)`);
502
+ }
503
+ else {
504
+ appName = await input({
505
+ message: "What would you like to name your app?",
506
+ default: defaultAppName,
124
507
  });
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
508
  }
141
- // Prompt for app name
142
- appName = await input({
143
- message: "What would you like to name your app?",
144
- default: defaultAppName,
145
- });
509
+ await prepareTemplateOrExit();
146
510
  // Create app
147
511
  info("Creating app on Primitive server...");
148
512
  try {
@@ -162,52 +526,68 @@ async function handleInit(projectDirArg, options) {
162
526
  catch (err) {
163
527
  error(`Failed to create app: ${err.message}`);
164
528
  error("You can try again or use --use-existing-app-id with an existing app.");
165
- warn(`Template files were extracted to "${targetDir}" but VITE_APP_ID is not configured.`);
166
- warn("You'll need to manually set VITE_APP_ID in your .env file to use this project.");
529
+ // The template archive has been validated in a temp directory, but it has
530
+ // not been installed into targetDir yet (#1218).
531
+ warn("No project files were written. Re-run `primitive init` after resolving the app creation error.");
532
+ cleanupPreparedTemplate();
167
533
  process.exit(1);
168
534
  }
169
535
  // Configure app access mode
170
- console.log();
171
- info("Let's configure who can access your app.");
172
- info("(You can change this later with 'primitive apps update')");
173
- const accessMode = await select({
174
- message: "Who should be able to sign up for your app?",
175
- choices: [
176
- {
177
- value: "public",
178
- name: "Anyone (public)",
179
- description: "Anyone can create an account",
180
- },
181
- {
182
- value: "domain",
183
- name: "Domain restricted",
184
- description: "Only users with email addresses from specific domains",
185
- },
186
- {
187
- value: "invite-only",
188
- name: "Invite only",
189
- description: "Only users you explicitly invite can sign up",
190
- },
191
- ],
192
- default: "invite-only",
193
- });
536
+ let accessMode;
537
+ if (initConfig?.access_mode) {
538
+ accessMode = initConfig.access_mode;
539
+ info(`Access mode: ${accessMode} (from config)`);
540
+ }
541
+ else {
542
+ console.log();
543
+ info("Let's configure who can access your app.");
544
+ info("(You can change this later with 'primitive apps update')");
545
+ accessMode = await select({
546
+ message: "Who should be able to sign up for your app?",
547
+ choices: [
548
+ {
549
+ value: "public",
550
+ name: "Anyone (public)",
551
+ description: "Anyone can create an account",
552
+ },
553
+ {
554
+ value: "domain",
555
+ name: "Domain restricted",
556
+ description: "Only users with email addresses from specific domains",
557
+ },
558
+ {
559
+ value: "invite-only",
560
+ name: "Invite only",
561
+ description: "Only users you explicitly invite can sign up",
562
+ },
563
+ ],
564
+ default: "invite-only",
565
+ });
566
+ }
194
567
  const client = new ApiClient();
195
568
  const settingsPayload = {
196
569
  mode: accessMode,
197
570
  };
198
571
  if (accessMode === "domain") {
199
- const domainsInput = await input({
200
- message: "Enter allowed email domain(s) (comma-separated, e.g., 'acme.com, example.org'):",
201
- validate: (value) => {
202
- if (!value.trim())
203
- return "Please enter at least one domain";
204
- return true;
205
- },
206
- });
207
- const allowedDomains = domainsInput
208
- .split(",")
209
- .map((d) => d.trim().toLowerCase())
210
- .filter((d) => d.length > 0);
572
+ let allowedDomains;
573
+ if (initConfig?.allowed_domains && initConfig.allowed_domains.length > 0) {
574
+ allowedDomains = initConfig.allowed_domains;
575
+ info(`Allowed domains: ${allowedDomains.join(", ")} (from config)`);
576
+ }
577
+ else {
578
+ const domainsInput = await input({
579
+ message: "Enter allowed email domain(s) (comma-separated, e.g., 'acme.com, example.org'):",
580
+ validate: (value) => {
581
+ if (!value.trim())
582
+ return "Please enter at least one domain";
583
+ return true;
584
+ },
585
+ });
586
+ allowedDomains = domainsInput
587
+ .split(",")
588
+ .map((d) => d.trim().toLowerCase())
589
+ .filter((d) => d.length > 0);
590
+ }
211
591
  settingsPayload.allowedDomains = allowedDomains;
212
592
  }
213
593
  // Update app settings with the chosen mode
@@ -218,34 +598,57 @@ async function handleInit(projectDirArg, options) {
218
598
  catch (err) {
219
599
  warn(`Could not update app settings: ${err.message}`);
220
600
  }
221
- // If invite-only, offer to add initial allowed users
601
+ // If invite-only, auto-add the authenticated user and offer to add others
222
602
  if (accessMode === "invite-only") {
223
- const inviteNow = await confirm({
224
- message: "Would you like to add initial users who can sign up? (no emails will be sent)",
225
- default: false,
226
- });
227
- if (inviteNow) {
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
- },
603
+ try {
604
+ await client.createInvitation(appId, {
605
+ email: credentials.email,
606
+ role: "member",
607
+ sendEmail: false,
241
608
  });
242
- const emails = emailsInput
243
- .split(",")
244
- .map((e) => e.trim())
245
- .filter((e) => e.length > 0);
609
+ success(`${credentials.email} added to the invite list.`);
610
+ }
611
+ catch (err) {
612
+ if (!err.message?.includes("already has a pending invitation")) {
613
+ warn(`Could not add your email to the invite list: ${err.message}`);
614
+ }
615
+ }
616
+ // Determine emails to invite
617
+ let emailsToInvite = [];
618
+ if (initConfig?.invite_emails && initConfig.invite_emails.length > 0) {
619
+ emailsToInvite = initConfig.invite_emails;
620
+ info(`Inviting users from config: ${emailsToInvite.join(", ")}`);
621
+ }
622
+ else if (!initConfig) {
623
+ const inviteNow = await confirm({
624
+ message: "Would you like to add initial users who can sign up? (no emails will be sent)",
625
+ default: false,
626
+ });
627
+ if (inviteNow) {
628
+ const emailsInput = await input({
629
+ message: "Enter email address(es) to allow (comma-separated):",
630
+ validate: (value) => {
631
+ if (!value.trim())
632
+ return "Please enter at least one email";
633
+ const emails = value.split(",").map((e) => e.trim());
634
+ for (const email of emails) {
635
+ if (!email.includes("@")) {
636
+ return `Invalid email: ${email}`;
637
+ }
638
+ }
639
+ return true;
640
+ },
641
+ });
642
+ emailsToInvite = emailsInput
643
+ .split(",")
644
+ .map((e) => e.trim())
645
+ .filter((e) => e.length > 0);
646
+ }
647
+ }
648
+ if (emailsToInvite.length > 0) {
246
649
  info("Adding invited users...");
247
650
  let inviteCount = 0;
248
- for (const email of emails) {
651
+ for (const email of emailsToInvite) {
249
652
  try {
250
653
  await client.createInvitation(appId, {
251
654
  email,
@@ -264,98 +667,305 @@ async function handleInit(projectDirArg, options) {
264
667
  }
265
668
  }
266
669
  }
267
- // Step 5: Update .env file
268
- info("Configuring environment...");
670
+ // Step 3 (deferred): install the already-prepared template + init git, now
671
+ // that the app exists. The network download and archive validation already
672
+ // happened before createApp, without touching targetDir.
673
+ info(`Setting up your ${platform === "ios" ? "iOS" : "web"} project...`);
269
674
  try {
270
- const envResult = updateEnvFile(targetDir, appId);
271
- if (!envResult.envUpdated) {
272
- warn("No .env or .env.example file found in template.");
273
- warn(`Please manually set VITE_APP_ID=${appId} in your .env file.`);
274
- }
275
- else if (!envResult.envProductionUpdated) {
276
- info(".env configured. No .env.production file found (this is optional).");
277
- }
278
- else {
279
- success("Environment configured (.env and .env.production).");
280
- }
675
+ preparedTemplate.install(targetDir);
676
+ success("Template extracted successfully.");
281
677
  }
282
678
  catch (err) {
283
- warn(`Could not update .env file: ${err.message}`);
284
- warn(`Please manually set VITE_APP_ID=${appId} in your .env file.`);
285
- }
286
- // Step 6: Check if pnpm is installed globally, offer to install if not
287
- const pnpmInstalled = await isPnpmInstalled();
288
- if (!pnpmInstalled) {
289
- console.log();
290
- info("pnpm is not installed globally on your system.");
291
- const installPnpm = await confirm({
292
- message: "Install pnpm globally? (recommended for future use)",
293
- default: true,
294
- });
295
- if (installPnpm) {
296
- info("Installing pnpm...");
679
+ error(`Failed to write template files: ${err.message}`);
680
+ cleanupPreparedTemplate();
681
+ process.exit(1);
682
+ }
683
+ finally {
684
+ cleanupPreparedTemplate();
685
+ }
686
+ // Initialize git repository (commit happens later, after env + install)
687
+ try {
688
+ await gitInitRepo(targetDir);
689
+ }
690
+ catch {
691
+ // Non-fatal: git may not be installed, or already a git repo
692
+ }
693
+ // Step 4.5: Update CLI to point at this app
694
+ try {
695
+ setCurrentApp(appId, appName);
696
+ info(`CLI now using app: ${appName} (${appId})`);
697
+ }
698
+ catch (err) {
699
+ // Not critical - user may not be logged in (e.g. used --use-existing-app-id without login)
700
+ warn(`Could not set current app in CLI config: ${err.message}`);
701
+ }
702
+ // Steps 5-8: Platform-specific configuration and dependency installation
703
+ if (isApple) {
704
+ // ── Apple (SwiftUI) path ──────────────────────────────────────────
705
+ // Step 5: Update primitive.json
706
+ info("Configuring primitive.json...");
707
+ try {
708
+ const updated = updatePrimitiveJson(targetDir, appId, serverUrl, appName);
709
+ if (!updated) {
710
+ warn("No primitive.json file found in template.");
711
+ warn(`Please manually set appId to "${appId}" in your primitive.json.`);
712
+ }
713
+ else {
714
+ success("primitive.json configured.");
715
+ }
716
+ }
717
+ catch (err) {
718
+ warn(`Could not update primitive.json: ${err.message}`);
719
+ warn(`Please manually set appId to "${appId}" in your primitive.json.`);
720
+ }
721
+ // Step 6-7: Skip port selection and pnpm (not applicable to Apple)
722
+ // Step 8: Resolve Swift packages
723
+ if (!options.skipInstall) {
724
+ info("Resolving Swift packages (this may take a few minutes)...");
297
725
  try {
298
- await installPnpmGlobally();
299
- success("pnpm installed globally. You can now use 'pnpm' commands.");
726
+ await runSwiftPackageResolve(targetDir);
727
+ success("Swift packages resolved.");
300
728
  }
301
729
  catch (err) {
302
- warn(`Could not install pnpm: ${err.message}`);
303
- warn("Will use npx to run pnpm for dependency installation.");
730
+ warn(`Swift package resolution failed: ${err.message}`);
731
+ warn("You can try running 'swift package resolve' manually.");
304
732
  }
305
733
  }
734
+ else {
735
+ info("Skipping dependency installation (--skip-install).");
736
+ }
306
737
  }
307
- // Step 7: Install dependencies with pnpm (via npx to handle version management)
308
- if (!options.skipInstall) {
309
- info("Installing dependencies...");
738
+ else {
739
+ // ── Web path ──────────────────────────────────────────────────────
740
+ // Step 5: Update .env file
741
+ info("Configuring environment...");
310
742
  try {
311
- await runPnpmInstall(targetDir);
312
- success("Dependencies installed.");
743
+ const envResult = updateEnvFile(targetDir, appId, serverUrl, appName);
744
+ if (!envResult.envUpdated) {
745
+ warn("No .env or .env.example file found in template.");
746
+ warn(`Please manually set VITE_APP_ID=${appId} in your .env file.`);
747
+ }
748
+ else if (!envResult.envProductionUpdated) {
749
+ info(".env configured. No .env.production file found (this is optional).");
750
+ }
751
+ else {
752
+ success("Environment configured (.env and .env.production).");
753
+ }
313
754
  }
314
755
  catch (err) {
315
- warn(`Dependency installation failed: ${err.message}`);
316
- warn("You can try running 'npx pnpm install' manually.");
756
+ warn(`Could not update .env file: ${err.message}`);
757
+ warn(`Please manually set VITE_APP_ID=${appId} in your .env file.`);
758
+ }
759
+ // Step 6: Confirm the dev port to use. Default to 5173; warn about taken ports.
760
+ try {
761
+ console.log();
762
+ const defaultPort = 5173;
763
+ const defaultOrigin = `http://localhost:${defaultPort}`;
764
+ // Scan up to 10 ports starting from 5173 to surface what's already in use
765
+ const takenPorts = [];
766
+ for (let p = defaultPort; p < defaultPort + 10; p++) {
767
+ if (!(await isPortFree(p)))
768
+ takenPorts.push(p);
769
+ else
770
+ break;
771
+ }
772
+ if (takenPorts.length > 0) {
773
+ warn(`Port${takenPorts.length > 1 ? "s" : ""} ${takenPorts.join(", ")} ${takenPorts.length > 1 ? "are" : "is"} already in use.`);
774
+ }
775
+ const portInput = await input({
776
+ message: "Dev server port for this project:",
777
+ default: String(defaultPort),
778
+ validate: (val) => {
779
+ const n = parseInt(val, 10);
780
+ if (isNaN(n) || n < 1 || n > 65535)
781
+ return "Enter a valid port number.";
782
+ return true;
783
+ },
784
+ });
785
+ const chosenPort = parseInt(portInput, 10);
786
+ if (chosenPort !== defaultPort) {
787
+ const devOrigin = `http://localhost:${chosenPort}`;
788
+ // Pin the port in vite.config.ts and update .env
789
+ updateViteConfig(targetDir, chosenPort);
790
+ updateEnvPort(targetDir, chosenPort);
791
+ // Update app settings: CORS, redirect URIs, baseUrl
792
+ if (!credentials) {
793
+ info(`Local files updated. Skipping server CORS/redirect config — not authenticated.`);
794
+ }
795
+ else {
796
+ try {
797
+ const client = new ApiClient();
798
+ let existingOrigins = [];
799
+ let existingRedirectUris = [];
800
+ try {
801
+ const currentSettings = await client.getAppSettings(appId);
802
+ if (currentSettings.corsMode === "custom" &&
803
+ Array.isArray(currentSettings.corsAllowedOrigins)) {
804
+ existingOrigins = currentSettings.corsAllowedOrigins;
805
+ }
806
+ if (Array.isArray(currentSettings.redirectUris)) {
807
+ existingRedirectUris = currentSettings.redirectUris;
808
+ }
809
+ }
810
+ catch {
811
+ // Ignore — will set a fresh config
812
+ }
813
+ // Always include 5173 since it's the universal localhost default
814
+ const corsOrigins = Array.from(new Set([...existingOrigins, defaultOrigin, devOrigin]));
815
+ const redirectUris = Array.from(new Set([
816
+ ...existingRedirectUris,
817
+ `${defaultOrigin}/oauth/callback`,
818
+ `${devOrigin}/oauth/callback`,
819
+ ]));
820
+ await client.updateAppSettings(appId, {
821
+ corsMode: "custom",
822
+ corsAllowedOrigins: corsOrigins,
823
+ corsAllowCredentials: true,
824
+ redirectUris,
825
+ baseUrl: devOrigin,
826
+ });
827
+ success(`Dev port set to ${chosenPort}: vite.config.ts, .env, CORS, redirect URIs, and base URL updated.`);
828
+ }
829
+ catch (err) {
830
+ warn(`Could not update app settings: ${err.message}`);
831
+ }
832
+ }
833
+ }
834
+ }
835
+ catch {
836
+ // Non-fatal: port detection or settings update failed silently
837
+ }
838
+ // Step 7: Check if pnpm is installed globally, offer to install if not
839
+ const pnpmInstalled = await isPnpmInstalled();
840
+ if (!pnpmInstalled) {
841
+ console.log();
842
+ info("pnpm is not installed globally on your system.");
843
+ const installPnpm = await confirm({
844
+ message: "Install pnpm globally? (recommended for future use)",
845
+ default: true,
846
+ });
847
+ if (installPnpm) {
848
+ info("Installing pnpm...");
849
+ try {
850
+ await installPnpmGlobally();
851
+ success("pnpm installed globally. You can now use 'pnpm' commands.");
852
+ }
853
+ catch (err) {
854
+ warn(`Could not install pnpm: ${err.message}`);
855
+ warn("Will use npx to run pnpm for dependency installation.");
856
+ }
857
+ }
858
+ }
859
+ // Step 8: Install dependencies with pnpm (via npx to handle version management)
860
+ if (!options.skipInstall) {
861
+ info("Installing dependencies...");
862
+ try {
863
+ await runPnpmInstall(targetDir);
864
+ success("Dependencies installed.");
865
+ }
866
+ catch (err) {
867
+ warn(`Dependency installation failed: ${err.message}`);
868
+ warn("You can try running 'npx pnpm install' manually.");
869
+ }
870
+ }
871
+ else {
872
+ info("Skipping dependency installation (--skip-install).");
317
873
  }
318
874
  }
319
- else {
320
- info("Skipping dependency installation (--skip-install).");
321
- }
322
- // Step 8: Install primitive CLI globally if not already installed
875
+ // Step 9: Install primitive CLI globally if not already installed
323
876
  const cliInstalled = await isPrimitiveCliInstalled();
324
877
  if (!cliInstalled) {
325
- console.log();
326
- info("The Primitive CLI helps you manage your app from the command line.");
327
- const installCli = await confirm({
328
- message: "Install the Primitive CLI globally?",
329
- default: true,
330
- });
331
- if (installCli) {
878
+ if (initConfig) {
879
+ // Non-interactive: auto-install CLI
332
880
  info("Installing Primitive CLI...");
333
881
  try {
334
882
  await installPrimitiveCli();
335
- success("Primitive CLI installed. You can now use 'primitive' commands.");
883
+ success("Primitive CLI installed.");
336
884
  }
337
885
  catch (err) {
338
886
  warn(`Could not install CLI: ${err.message}`);
339
887
  warn("You can install it manually with: npm install -g primitive-admin");
340
888
  }
341
889
  }
890
+ else {
891
+ console.log();
892
+ info("The Primitive CLI helps you manage your app from the command line.");
893
+ const installCli = await confirm({
894
+ message: "Install the Primitive CLI globally?",
895
+ default: true,
896
+ });
897
+ if (installCli) {
898
+ info("Installing Primitive CLI...");
899
+ try {
900
+ await installPrimitiveCli();
901
+ success("Primitive CLI installed. You can now use 'primitive' commands.");
902
+ }
903
+ catch (err) {
904
+ warn(`Could not install CLI: ${err.message}`);
905
+ warn("You can install it manually with: npm install -g primitive-admin");
906
+ }
907
+ }
908
+ }
909
+ }
910
+ // Step 10: Offer to install Claude Code skill if not already installed
911
+ try {
912
+ const { isSkillInstalled, installSkillExplicit, checkSkillStatus } = await import("../lib/skill-installer.js");
913
+ if (isSkillInstalled()) {
914
+ // Already installed — silently auto-update if a newer version is bundled
915
+ await checkSkillStatus();
916
+ }
917
+ else if (initConfig) {
918
+ // Non-interactive: auto-install skill
919
+ installSkillExplicit();
920
+ }
921
+ else {
922
+ console.log();
923
+ info("The Primitive platform skill gives Claude Code expertise with Primitive APIs and patterns.");
924
+ const installSkill = await confirm({
925
+ message: "Install the Primitive platform skill for Claude Code?",
926
+ default: true,
927
+ });
928
+ if (installSkill) {
929
+ installSkillExplicit();
930
+ }
931
+ }
342
932
  }
343
- // Step 9: Show success message
933
+ catch {
934
+ // Non-fatal: skill installation should never block init
935
+ }
936
+ // Step 11: Create initial git commit (captures env config + installed dependencies)
937
+ try {
938
+ await gitInitialCommit(targetDir);
939
+ }
940
+ catch {
941
+ // Non-fatal: git may not be installed, or already committed
942
+ }
943
+ // Step 12: Show success message
344
944
  divider();
345
945
  success("Project initialized successfully!");
346
946
  divider();
347
947
  keyValue("Directory", targetDir);
348
948
  keyValue("App Name", appName);
349
949
  keyValue("App ID", appId);
950
+ keyValue("Platform", platform === "ios" ? "iOS (SwiftUI)" : "Web");
350
951
  divider();
351
952
  info("Next steps:");
352
953
  if (projectDir !== ".") {
353
954
  console.log(` cd ${projectDir}`);
354
955
  }
355
- if (options.skipInstall) {
356
- console.log(" pnpm install");
956
+ if (isApple) {
957
+ if (options.skipInstall) {
958
+ console.log(" swift package resolve");
959
+ }
960
+ console.log(" open *.xcodeproj");
961
+ console.log(" # or: ./run-ios.sh (iOS Simulator)");
962
+ }
963
+ else {
964
+ if (options.skipInstall) {
965
+ console.log(" pnpm install");
966
+ }
967
+ console.log(" pnpm dev");
357
968
  }
358
- console.log(" pnpm run dev");
359
969
  console.log();
360
970
  }
361
971
  //# sourceMappingURL=init.js.map