primitive-admin 1.1.0-alpha.3 → 1.1.0-alpha.31

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