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.
- package/README.md +168 -21
- package/assets/skill/skills/primitive-platform/SKILL.md +226 -0
- package/dist/bin/primitive.js +198 -15
- package/dist/bin/primitive.js.map +1 -1
- package/dist/src/commands/admins.js +107 -0
- package/dist/src/commands/admins.js.map +1 -1
- package/dist/src/commands/analytics.js +464 -55
- package/dist/src/commands/analytics.js.map +1 -1
- package/dist/src/commands/apps.js +27 -13
- package/dist/src/commands/apps.js.map +1 -1
- package/dist/src/commands/auth.js +165 -5
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/blob-buckets.js +354 -0
- package/dist/src/commands/blob-buckets.js.map +1 -0
- package/dist/src/commands/catalog.js +17 -17
- package/dist/src/commands/catalog.js.map +1 -1
- package/dist/src/commands/collection-type-configs.js +178 -0
- package/dist/src/commands/collection-type-configs.js.map +1 -0
- package/dist/src/commands/collections.js +526 -0
- package/dist/src/commands/collections.js.map +1 -0
- package/dist/src/commands/comparisons.js +6 -6
- package/dist/src/commands/comparisons.js.map +1 -1
- package/dist/src/commands/cron-triggers.js +364 -0
- package/dist/src/commands/cron-triggers.js.map +1 -0
- package/dist/src/commands/database-types.js +462 -0
- package/dist/src/commands/database-types.js.map +1 -0
- package/dist/src/commands/databases.js +1067 -58
- package/dist/src/commands/databases.js.map +1 -1
- package/dist/src/commands/documents.js +510 -2
- package/dist/src/commands/documents.js.map +1 -1
- package/dist/src/commands/email-templates.js +281 -0
- package/dist/src/commands/email-templates.js.map +1 -0
- package/dist/src/commands/env.js +260 -0
- package/dist/src/commands/env.js.map +1 -0
- package/dist/src/commands/group-type-configs.js +189 -0
- package/dist/src/commands/group-type-configs.js.map +1 -0
- package/dist/src/commands/groups.js +28 -25
- package/dist/src/commands/groups.js.map +1 -1
- package/dist/src/commands/init.js +719 -188
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/integrations.js +504 -33
- package/dist/src/commands/integrations.js.map +1 -1
- package/dist/src/commands/prompts.js +17 -16
- package/dist/src/commands/prompts.js.map +1 -1
- package/dist/src/commands/rule-sets.js +366 -0
- package/dist/src/commands/rule-sets.js.map +1 -0
- package/dist/src/commands/secrets.js +108 -0
- package/dist/src/commands/secrets.js.map +1 -0
- package/dist/src/commands/skill.js +29 -0
- package/dist/src/commands/skill.js.map +1 -0
- package/dist/src/commands/sync.js +2363 -182
- package/dist/src/commands/sync.js.map +1 -1
- package/dist/src/commands/tokens.js +9 -9
- package/dist/src/commands/tokens.js.map +1 -1
- package/dist/src/commands/users.js +426 -4
- package/dist/src/commands/users.js.map +1 -1
- package/dist/src/commands/webhooks.js +386 -0
- package/dist/src/commands/webhooks.js.map +1 -0
- package/dist/src/commands/workflows.js +614 -64
- package/dist/src/commands/workflows.js.map +1 -1
- package/dist/src/lib/api-client.js +942 -83
- package/dist/src/lib/api-client.js.map +1 -1
- package/dist/src/lib/config.js +51 -53
- package/dist/src/lib/config.js.map +1 -1
- package/dist/src/lib/constants.js +3 -0
- package/dist/src/lib/constants.js.map +1 -0
- package/dist/src/lib/credentials-store.js +307 -0
- package/dist/src/lib/credentials-store.js.map +1 -0
- package/dist/src/lib/env-resolver.js +121 -0
- package/dist/src/lib/env-resolver.js.map +1 -0
- package/dist/src/lib/init-config.js +87 -0
- package/dist/src/lib/init-config.js.map +1 -0
- package/dist/src/lib/migration-nag.js +163 -0
- package/dist/src/lib/migration-nag.js.map +1 -0
- package/dist/src/lib/output.js +58 -6
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/paginate.js +42 -0
- package/dist/src/lib/paginate.js.map +1 -0
- package/dist/src/lib/project-config.js +209 -0
- package/dist/src/lib/project-config.js.map +1 -0
- package/dist/src/lib/refresh-admin-credentials.js +103 -0
- package/dist/src/lib/refresh-admin-credentials.js.map +1 -0
- package/dist/src/lib/skill-installer.js +135 -0
- package/dist/src/lib/skill-installer.js.map +1 -0
- package/dist/src/lib/sync-paths.js +102 -0
- package/dist/src/lib/sync-paths.js.map +1 -0
- package/dist/src/lib/template.js +279 -18
- package/dist/src/lib/template.js.map +1 -1
- package/dist/src/lib/toml-database-config.js +384 -0
- package/dist/src/lib/toml-database-config.js.map +1 -0
- package/dist/src/lib/toml-params-validator.js +183 -0
- package/dist/src/lib/toml-params-validator.js.map +1 -0
- package/dist/src/lib/version-check.js +172 -0
- package/dist/src/lib/version-check.js.map +1 -0
- package/dist/src/lib/workflow-fragments.js +121 -0
- package/dist/src/lib/workflow-fragments.js.map +1 -0
- package/dist/src/lib/workflow-toml-validator.js +328 -0
- package/dist/src/lib/workflow-toml-validator.js.map +1 -0
- package/package.json +7 -4
|
@@ -1,70 +1,390 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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 {
|
|
8
|
-
import {
|
|
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
|
|
17
|
-
.option("--skip-install", "Skip
|
|
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
|
|
21
|
-
$ primitive init
|
|
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
|
|
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
|
|
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: "
|
|
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 "
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
545
|
+
// If invite-only, auto-add the authenticated user and offer to add others
|
|
222
546
|
if (accessMode === "invite-only") {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const emailsInput = await input({
|
|
229
|
-
message: "Enter email address(es) to allow (comma-separated):",
|
|
230
|
-
validate: (value) => {
|
|
231
|
-
if (!value.trim())
|
|
232
|
-
return "Please enter at least one email";
|
|
233
|
-
const emails = value.split(",").map((e) => e.trim());
|
|
234
|
-
for (const email of emails) {
|
|
235
|
-
if (!email.includes("@")) {
|
|
236
|
-
return `Invalid email: ${email}`;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
return true;
|
|
240
|
-
},
|
|
547
|
+
try {
|
|
548
|
+
await client.createInvitation(appId, {
|
|
549
|
+
email: credentials.email,
|
|
550
|
+
role: "member",
|
|
551
|
+
sendEmail: false,
|
|
241
552
|
});
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
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
|
|
268
|
-
info("Configuring environment...");
|
|
614
|
+
// Step 4.5: Update CLI to point at this app
|
|
269
615
|
try {
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
284
|
-
warn(`
|
|
285
|
-
}
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
info("
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
|
299
|
-
success("
|
|
647
|
+
await runSwiftPackageResolve(targetDir);
|
|
648
|
+
success("Swift packages resolved.");
|
|
300
649
|
}
|
|
301
650
|
catch (err) {
|
|
302
|
-
warn(`
|
|
303
|
-
warn("
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
659
|
+
else {
|
|
660
|
+
// ── Web path ──────────────────────────────────────────────────────
|
|
661
|
+
// Step 5: Update .env file
|
|
662
|
+
info("Configuring environment...");
|
|
310
663
|
try {
|
|
311
|
-
|
|
312
|
-
|
|
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(`
|
|
316
|
-
warn(
|
|
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
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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.
|
|
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
|
|
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 (
|
|
356
|
-
|
|
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
|