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