primitive-app 3.2.0-alpha.2 → 3.2.0-alpha.20

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.
@@ -12,20 +12,62 @@
12
12
  * gate if this committed copy drifts from the source. Edit the CLI file and
13
13
  * regenerate; edits made here are overwritten.
14
14
  */
15
- /** Schema version of `.primitive/config.json` this core understands. */
15
+ /** Schema version of `primitive/config.json` this core understands. */
16
16
  export declare const PRIMITIVE_CONFIG_VERSION = 1;
17
17
  /** Schema version of `.primitive/local.json` this core writes and reads. */
18
18
  export declare const PRIMITIVE_LOCAL_STATE_VERSION = 1;
19
- export declare const PROJECT_CONFIG_DIR = ".primitive";
19
+ /**
20
+ * The two directories, split apart by #3153 so one name never means two
21
+ * things: `primitive/` is the COMMITTED tree (the config and every
22
+ * environment's TOML), `.primitive/` is MACHINE-LOCAL state (credentials, this
23
+ * machine's selection, snapshot backups) and is gitignored whole.
24
+ */
25
+ export declare const PROJECT_TREE_DIR = "primitive";
26
+ export declare const PROJECT_LOCAL_STATE_DIR = ".primitive";
20
27
  export declare const PROJECT_CONFIG_FILENAME = "config.json";
21
28
  export declare const LOCAL_STATE_FILENAME = "local.json";
22
- export declare const PROJECT_CONFIG_DISPLAY_NAME = ".primitive/config.json";
29
+ export declare const PROJECT_CONFIG_DISPLAY_NAME = "primitive/config.json";
23
30
  export declare const LOCAL_STATE_DISPLAY_NAME = ".primitive/local.json";
31
+ /**
32
+ * The one refusal for a project that never migrated off the pre-#3153 layout:
33
+ * a `.primitive/config.json` with no `primitive/config.json` beside it.
34
+ *
35
+ * That shape is refused rather than walked past because the walk's next stop
36
+ * is the parent directory, and in a nested checkout the parent may be a
37
+ * DIFFERENT project — a command run in the unmigrated one would silently act
38
+ * on the other's app. Nothing else is refused (#3392): anything left under
39
+ * `.primitive/` beside a migrated tree is machine-local junk, not signal.
40
+ *
41
+ * One sentence, naming both halves — the file that was found and the file
42
+ * that is expected — and no document. The migration page this once pointed
43
+ * at was never published (#3155, won't-fix), and every reader that shares
44
+ * this refusal — this core, the generated Vite-plugin copy, the Swift
45
+ * pre-build script, the Vue template's deploy script — says the same thing.
46
+ */
47
+ export declare function unmigratedProjectMessage(found: string, expected: string): string;
48
+ /**
49
+ * The one refusal for a command run outside a Primitive project (#3154,
50
+ * intent criterion 5).
51
+ *
52
+ * There is exactly one way to resolve the configuration tree, the credentials
53
+ * and the app: the environment named by `primitive/config.json`. Outside a
54
+ * project there is no such thing, and the CLI says so once, in these words,
55
+ * wherever the question is asked — the resolver core, the CLI's
56
+ * `requireCurrentEnvironment()`, and the credentials store.
57
+ *
58
+ * Two constraints on the wording, both test-asserted:
59
+ *
60
+ * - it names `primitive init`, the one command that creates what is missing;
61
+ * - it never names `primitive env add`, which after this change also fails
62
+ * outside a project — an error may only offer an invocation it does not
63
+ * itself block.
64
+ */
65
+ export declare function missingConfigMessage(where: string): string;
24
66
  /**
25
67
  * Error kinds callers branch on. Every failure is one of these — no reader is
26
68
  * ever left guessing what a broken config meant.
27
69
  */
28
- export type PrimitiveEnvErrorKind = "missing-config" | "malformed-config" | "unsupported-version" | "corrupt-local-state" | "unknown-environment" | "no-selection" | "missing-app-id";
70
+ export type PrimitiveEnvErrorKind = "missing-config" | "malformed-config" | "unsupported-version" | "corrupt-local-state" | "unknown-environment" | "no-selection" | "missing-app-id" | "stale-layout";
29
71
  export declare class PrimitiveEnvError extends Error {
30
72
  readonly kind: PrimitiveEnvErrorKind;
31
73
  readonly path?: string;
@@ -69,7 +111,7 @@ export interface CoreEnvironmentEntry {
69
111
  *
70
112
  * `primitive env add --web-url` and the project-config validation reject those
71
113
  * shapes with a field-named error, but neither is on the path from a
72
- * hand-edited `.primitive/config.json` to an Xcode build: the Swift template's
114
+ * hand-edited `primitive/config.json` to an Xcode build: the Swift template's
73
115
  * pre-build script reads the file directly. So every reader that feeds
74
116
  * `primitive.json` applies the contract, and a value that misses it is read as
75
117
  * "this environment has no web counterpart" — code-only email, no trusted
@@ -113,6 +155,14 @@ export interface ResolvedPrimitiveEnv {
113
155
  /** The app's web counterpart for the SELECTED environment, if it has one. */
114
156
  webUrl?: string;
115
157
  description?: string;
158
+ /**
159
+ * The committed environment whose `primitive/<tree>/` this one reads and
160
+ * writes: its own name for a committed environment, the entry's `tree` for
161
+ * a machine-local one (#4117).
162
+ */
163
+ tree: string;
164
+ /** Whether this is a machine-local environment from `.primitive/local.json`. */
165
+ local: boolean;
116
166
  source: PrimitiveEnvSource;
117
167
  configPath: string;
118
168
  localStatePath: string;
@@ -135,45 +185,114 @@ export interface ResolveOptions {
135
185
  */
136
186
  export declare function deriveWsUrl(apiUrl: string): string;
137
187
  /**
138
- * Finds `.primitive/config.json` by walking up from `cwd`, or returns the
188
+ * Finds `primitive/config.json` by walking up from `cwd`, or returns the
139
189
  * PRIMITIVE_PROJECT_CONFIG override when it is set (and exists).
190
+ *
191
+ * The override is checked for provenance BEFORE the file is read (#3153, F2):
192
+ * it bypasses the walk, so without this an old-layout config could be handed
193
+ * straight to a reader and defeat the cutover. An override that points inside
194
+ * a `.primitive/` directory IS the old layout, named explicitly.
195
+ *
196
+ * This is the one function every reader goes through — `resolvePrimitiveEnv`
197
+ * and the CLI's own `loadProjectConfig`/`findProjectRoot` alike — so the
198
+ * unmigrated-project refusal lives HERE, in the walk, or the commands that
199
+ * never call `resolvePrimitiveEnv` would walk past the old anchor into the
200
+ * parent directory's project.
140
201
  */
141
202
  export declare function findPrimitiveConfigPath(options?: {
142
203
  cwd?: string;
143
204
  env?: Record<string, string | undefined>;
144
205
  }): string | null;
145
206
  /**
146
- * The project root for a config path: the parent of `.primitive/`, or the
207
+ * The project root for a config path: the parent of `primitive/`, or the
147
208
  * containing directory when the config was pointed at directly (a fixture via
148
- * PRIMITIVE_PROJECT_CONFIG, which is not inside a `.primitive/` directory).
209
+ * PRIMITIVE_PROJECT_CONFIG, which is not inside a `primitive/` directory).
149
210
  */
150
211
  export declare function projectRootForConfigPath(configPath: string): string;
151
212
  /**
152
- * Local state lives beside the config it belongs to, so a fixture-pointed
153
- * config never picks up a real project's selection and vice versa.
213
+ * Machine-local state lives under the project root's `.primitive/`, not beside
214
+ * the config — the config is committed now, and the two must not share a
215
+ * directory. A fixture-pointed bare config still gets its own `.primitive/`
216
+ * in its own directory, so it never picks up a real project's selection.
154
217
  */
155
218
  export declare function localStatePathForConfigPath(configPath: string): string;
156
219
  /**
157
- * Reads and structurally validates `.primitive/config.json`. Throws rather
220
+ * Reads and structurally validates `primitive/config.json`. Throws rather
158
221
  * than guessing: a config that cannot be understood is always louder than a
159
222
  * silent fallback to the wrong backend.
160
223
  */
161
224
  export declare function readCoreProjectConfig(configPath: string): CoreProjectConfig;
225
+ /**
226
+ * A machine-local environment (#4117): an entry in the `environments` map of
227
+ * `.primitive/local.json`. It names its own app and server, like a committed
228
+ * environment, and `tree` names the committed environment whose
229
+ * `primitive/<tree>/` it reads and writes — a child app sharing its parent's
230
+ * configuration tree. It is never committed, so it carries no web
231
+ * counterpart, description or iOS app id of its own.
232
+ */
233
+ export interface CoreLocalEnvironmentEntry {
234
+ apiUrl: string;
235
+ appId: string;
236
+ appName?: string;
237
+ tree: string;
238
+ }
239
+ /** What `.primitive/local.json` holds, read and validated. */
240
+ export interface CoreLocalState {
241
+ /** This machine's `primitive env use`, or null when nothing is selected. */
242
+ selectedEnvironment: string | null;
243
+ /** The machine-local environments, keyed by name. Empty when there are none. */
244
+ environments: Record<string, CoreLocalEnvironmentEntry>;
245
+ }
246
+ /**
247
+ * Reads `.primitive/local.json`: this machine's selection and its
248
+ * machine-local environments. Returns an empty state when there is no file;
249
+ * throws `corrupt-local-state` when the file exists but cannot be read as the
250
+ * shape the CLI writes.
251
+ *
252
+ * Every entry is validated here, at the read, whether or not it is the one
253
+ * selected: a broken entry is a broken file, and a reader that only checked
254
+ * the selected one would accept a file another reader then refuses. Given the
255
+ * committed config, an entry is also refused when its name is a committed
256
+ * environment's or its `tree` names none.
257
+ */
258
+ export declare function readLocalState(configPath: string, config?: CoreProjectConfig): CoreLocalState;
259
+ /**
260
+ * Validates the `environments` map of `.primitive/local.json` — absent reads
261
+ * as empty — and returns its entries normalized (`apiUrl` without a trailing
262
+ * slash, `appId` and `tree` trimmed). Every failure is `corrupt-local-state`
263
+ * naming the entry and the field. Exported so the CLI's write side refuses
264
+ * exactly what this read refuses.
265
+ */
266
+ export declare function parseLocalEnvironments(raw: unknown, path: string, config?: CoreProjectConfig): Record<string, CoreLocalEnvironmentEntry>;
267
+ /**
268
+ * Whether `name` is a committed environment: an own key of the parsed map, so
269
+ * a valid name like `constructor` is never mistaken for one through
270
+ * `Object.prototype`.
271
+ */
272
+ export declare function isCommittedEnvironment(config: {
273
+ environments: Record<string, unknown>;
274
+ }, name: string): boolean;
162
275
  /**
163
276
  * Reads this machine's selection from `.primitive/local.json`. Returns null
164
277
  * when there is no file or no selection in it; throws when the file exists but
165
- * cannot be read as the shape `primitive env use` writes.
278
+ * cannot be read as the shape the CLI writes.
166
279
  */
167
280
  export declare function readLocalSelection(configPath: string): string | null;
168
281
  /**
169
282
  * Applies the precedence to an already-loaded config. Exposed separately so
170
283
  * the CLI wrapper can run its own field-level validation first and still share
171
284
  * one ordering with every other reader.
285
+ *
286
+ * Machine-local environments take part in the three explicit steps — a name
287
+ * given, `PRIMITIVE_ENV`, this machine's selection — and never in the two
288
+ * fallbacks: `defaultEnvironment` is committed, and a local entry never
289
+ * changes what a project resolves when nothing is selected.
172
290
  */
173
291
  export declare function selectEnvironmentName(config: CoreProjectConfig, options: {
174
292
  explicitEnvName?: string | null;
175
293
  envVarName?: string | null;
176
294
  localSelection?: string | null;
295
+ localEnvironments?: Record<string, CoreLocalEnvironmentEntry>;
177
296
  configPath?: string;
178
297
  }): {
179
298
  name: string;
@@ -65,6 +65,6 @@ const observer = new MutationObserver(async (mutations, obs) => {
65
65
  });
66
66
 
67
67
  observer.observe(document.body, { childList: true, subtree: true });
68
- `},transformIndexHtml(e){if(!h||!d)return e;let t=`<style data-primitive-devtools-sonner>${g}</style>`;return e.replace(`</body>`,`${t}\n<script type="module" src="/@id/__x00__virtual:primitive-devtools-init"><\/script>\n</body>`)},hotUpdate({file:e,server:t}){if(e.endsWith(`.primitive-test.ts`)){let e=t.moduleGraph.getModuleById(c);e&&(t.moduleGraph.invalidateModule(e),t.ws.send({type:`full-reload`}))}}}}var f=`.primitive`,p=`config.json`,m=`local.json`,h=`.primitive/config.json`,g=`.primitive/local.json`,_=class extends Error{kind;path;constructor(e,t,n){super(t),this.name=`PrimitiveEnvError`,this.kind=e,this.path=n}};function v(e){if(typeof e!=`string`)return;let t=e.trim();if(!t)return;let n;try{n=new URL(t)}catch{return}if(!(n.protocol!==`https:`&&n.protocol!==`http:`)&&!(n.protocol===`http:`&&n.hostname!==`localhost`&&n.hostname!==`127.0.0.1`)&&!(n.username||n.password)&&!(n.pathname!==`/`&&n.pathname!==``)&&!(n.search||n.hash))return n.origin}function y(e,t){return`Environment "${e}" in ${t?`${h} (${t})`:h} has no "appId". Every environment names exactly one app — add "appId": "<app-id>" to that environment in ${h}. Find the app id in the Primitive admin UI, with 'primitive apps list' run from a directory outside this project, or as the residual "currentAppId" in .primitive/credentials.json if this environment was ever pointed at an app by the retired per-machine app selection.`}function b(e){if(typeof e==`string`)return e.trim()||void 0}function x(e){return e.startsWith(`https://`)?`wss://`+e.slice(8):e.startsWith(`http://`)?`ws://`+e.slice(7):e}function S(e={}){let n=(e.env??process.env).PRIMITIVE_PROJECT_CONFIG;if(n){let e=(0,a.resolve)(n);return(0,t.existsSync)(e)?e:null}let r=(0,a.resolve)(e.cwd??process.cwd());for(;;){let e=(0,a.join)(r,f,p);if((0,t.existsSync)(e))return e;let n=(0,a.dirname)(r);if(n===r)return null;r=n}}function C(e){let t=(0,a.dirname)(e);return(0,a.basename)(t)===`.primitive`?(0,a.dirname)(t):t}function w(e){return(0,a.join)((0,a.dirname)(e),m)}function T(e){if(!(0,t.existsSync)(e))throw new _(`missing-config`,`No ${h} found at ${e}. Run 'primitive init' to create one.`,e);let n;try{n=(0,t.readFileSync)(e,`utf-8`)}catch(t){throw new _(`malformed-config`,`Failed to read ${h} (${e}): ${t.message}`,e)}let r;try{r=JSON.parse(n)}catch(t){throw new _(`malformed-config`,`Failed to parse ${h} (${e}): ${t.message}`,e)}if(!r||typeof r!=`object`||Array.isArray(r))throw new _(`malformed-config`,`${h} (${e}) must contain a JSON object at the top level.`,e);let i=r;if(typeof i.version!=`number`||!Number.isInteger(i.version))throw new _(`malformed-config`,`${h} (${e}) is missing required integer field "version".`,e);if(i.version!==1)throw new _(`unsupported-version`,`${h} (${e}) has version ${i.version}, but this tool understands version 1.`,e);if(!i.environments||typeof i.environments!=`object`||Array.isArray(i.environments))throw new _(`malformed-config`,`${h} (${e}) is missing required "environments" object.`,e);let a={};for(let[t,n]of Object.entries(i.environments)){if(!n||typeof n!=`object`||Array.isArray(n))throw new _(`malformed-config`,`Environment "${t}" must be an object in ${h} (${e}).`,e);let r=n;if(typeof r.apiUrl!=`string`||!r.apiUrl)throw new _(`malformed-config`,`Environment "${t}" must have a non-empty "apiUrl" string in ${h} (${e}).`,e);let i=b(r.appId);if(!i)throw new _(`missing-app-id`,y(t,e),e);a[t]={apiUrl:r.apiUrl.replace(/\/$/,``),appId:i,appName:typeof r.appName==`string`?r.appName:void 0,webUrl:v(r.webUrl),description:typeof r.description==`string`?r.description:void 0}}return{version:i.version,defaultEnvironment:typeof i.defaultEnvironment==`string`&&i.defaultEnvironment?i.defaultEnvironment:void 0,environments:a}}function E(e){let n=w(e);if(!(0,t.existsSync)(n))return null;let r=e=>{throw new _(`corrupt-local-state`,`${g} (${n}) is unreadable: ${e}. Delete the file or re-run 'primitive env use <name>'.`,n)},i;try{i=JSON.parse((0,t.readFileSync)(n,`utf-8`))}catch(e){return r(e.message)}if(!i||typeof i!=`object`||Array.isArray(i))return r(`expected a JSON object`);let a=i.selectedEnvironment;return a==null||a===``?null:typeof a==`string`?a:r(`"selectedEnvironment" must be a string`)}function D(e,t){let n=Object.keys(e.environments),r=n.join(`, `)||`(none)`,i=t.configPath?`${h} (${t.configPath})`:h,a=(n,a,o)=>{if(!e.environments[n])throw new _(`unknown-environment`,`Environment "${n}" is not defined in ${i} (selected ${o}). Available: ${r}`,t.configPath);return{name:n,source:a}};if(t.explicitEnvName)return a(t.explicitEnvName,`explicit`,`explicitly`);if(t.envVarName)return a(t.envVarName,`env-var`,`via PRIMITIVE_ENV`);if(t.localSelection)return a(t.localSelection,`local`,`in ${g}`);if(e.defaultEnvironment)return a(e.defaultEnvironment,`default`,`as "defaultEnvironment"`);if(n.length===1)return{name:n[0],source:`sole`};throw n.length===0?new _(`no-selection`,`${i} has no environments defined. Run 'primitive env add <name>' or 'primitive init'.`,t.configPath):new _(`no-selection`,`No environment selected. Run 'primitive env use <name>', set "defaultEnvironment" in ${i}, or export PRIMITIVE_ENV. Available: ${r}`,t.configPath)}function O(e={}){let t=e.env??process.env,n=e.configPath??S({cwd:e.cwd,env:t});if(!n)throw new _(`missing-config`,`No ${h} found in ${e.cwd??process.cwd()} or any parent directory. Run 'primitive init' to create one, or 'primitive env add <name>' to add an environment.`);let r=T(n),{name:i,source:a}=D(r,{explicitEnvName:e.explicitEnvName||null,envVarName:t.PRIMITIVE_ENV||null,localSelection:E(n),configPath:n}),o=r.environments[i];return{name:i,apiUrl:o.apiUrl,wsUrl:x(o.apiUrl),appId:o.appId,appName:o.appName,webUrl:o.webUrl,description:o.description,source:a,configPath:n,localStatePath:w(n),projectRoot:C(n)}}var k=[`VITE_APP_ID`,`VITE_API_URL`,`VITE_WS_URL`,`VITE_APP_NAME`],A=`VITE_EXPECTED_PRIMITIVE_ENV`,j=new Map;function M(e,t){let n=process.env[e];n!==void 0&&n!==``&&j.get(e)!==n||(process.env[e]=t,j.set(e,t))}function N(e={}){return O({cwd:e.root,env:e.env,explicitEnvName:e.primitiveEnv??null})}function P(e,t,n){let r=e=>n.includes(e)?` (overridden)`:``,i=[`[primitive-env] Primitive environment: ${e.name} (selected: ${e.source})`,`[primitive-env] apiUrl: ${t.apiUrl}${r(`VITE_API_URL`)}`,`[primitive-env] wsUrl: ${t.wsUrl}${r(`VITE_WS_URL`)}`,`[primitive-env] appId: ${t.appId??`(unset)`}${r(`VITE_APP_ID`)}`,`[primitive-env] appName: ${t.appName??`(unset)`}${r(`VITE_APP_NAME`)}`,`[primitive-env] config: ${e.configPath}`];return n.length>0&&i.push(`[primitive-env] overridden by the environment: ${n.join(`, `)}`),i.join(`
69
- `)}function F(e,t){let n=e?.envDir;return n===!1?null:typeof n==`string`&&n!==``?(0,a.resolve)(t,n):t}function I(e,t){let n=process.env[A];if(n!==void 0&&n!==``)return{value:n,where:`the process environment`};if(t===null)return null;for(let n of[`.env.${e}.local`,`.env.${e}`,`.env.local`,`.env`]){let e=V((0,a.join)(t,n))[A];if(e!==void 0)return{value:e,where:n}}return null}function L(e,t,n){return[`[primitive-env] Wrong pair: vite mode "${e}" is declared to run against Primitive environment "${t.value}", but "${n.name}" resolved.`,`[primitive-env] expected: ${t.value} (${A} in ${t.where})`,`[primitive-env] resolved: ${n.name} (selected: ${n.source})`,`[primitive-env] config: ${n.configPath}`,``,`Run the pair you meant — PRIMITIVE_ENV=${t.value} <command> --mode ${e}, or 'primitive env use ${t.value}' — or change/remove ${A} in ${t.where}.`,``,`A mode declares this when its .env.${e} carries values coupled to one backend, so continuing would run this environment's identity with another's configuration.`].join(`
70
- `)}function R(e,t,n){let r=I(e,t);if(r&&r.value!==``&&r.value!==n.name)throw Error(L(e,r,n))}function z(e={}){let t=null,n=null,r,i=[],a=!1,o=null;return{name:`primitive-env`,enforce:`pre`,config(s,c){a=c?.command===`serve`,o=null;let l=s?.root??process.cwd(),u=c?.mode??`development`,d=B(u,l),f={};for(let e of k){let t=process.env[e],n=t!==void 0&&j.get(e)!==t?t:d[e];n!==void 0&&n!==``&&(f[e]=n)}let p;try{p=O({cwd:l,explicitEnvName:e.primitiveEnv??null})}catch(e){if(e instanceof _&&e.kind===`missing-config`&&f.VITE_APP_ID&&f.VITE_API_URL)return console.warn(`[primitive-env] No .primitive/config.json found; using VITE_APP_ID / VITE_API_URL from the environment. Run 'primitive init' to make the project the source of truth.`),r=f.VITE_APP_NAME,{};throw e}if(!(f.VITE_APP_ID&&f.VITE_API_URL)){let e=F(s,l);R(u,e,p),o={root:l,mode:u,dir:e}}t=p,r=f.VITE_APP_NAME??p.appName;let m={VITE_APP_ID:p.appId,VITE_API_URL:p.apiUrl,VITE_WS_URL:p.wsUrl,VITE_APP_NAME:p.appName},h={"import.meta.env.VITE_PRIMITIVE_ENV":JSON.stringify(p.name)},g=[];for(let e of k){if(f[e]!==void 0){g.push(e);continue}let t=m[e];t!==void 0&&(h[`import.meta.env.${e}`]=JSON.stringify(t),M(e,t))}return M(`VITE_PRIMITIVE_ENV`,p.name),i=g,n={apiUrl:f.VITE_API_URL??p.apiUrl,wsUrl:f.VITE_WS_URL??p.wsUrl,appId:f.VITE_APP_ID??p.appId,appName:r},e.logResolved!==!1&&console.log(P(p,n,g)),g.length>0&&console.warn(`[primitive-env] Overridden by an explicit value in .env or the shell: ${g.join(`, `)}. Remove it to use .primitive/config.json (deploys reject these outright).`),{define:h}},configResolved(e){if(!o||!t)return;let n=F(e,o.root),r=e?.mode??o.mode;n===o.dir&&r===o.mode||(R(r,n,t),o={root:o.root,mode:r,dir:n})},transformIndexHtml(){if(!a||!t||!n)return[];let e=`%c[primitive] env=${t.name} api=${n.apiUrl} app=${n.appId??`(unset)`} (${t.configPath})`+(i.length>0?` overridden: ${i.join(`, `)}`:``);return[{tag:`script`,injectTo:`head`,children:`console.info(${JSON.stringify(e)}, "color:#888");`}]},appName(){return r},resolved(){return t}}}function B(e,t){let n=[`.env`,`.env.local`,`.env.${e}`,`.env.${e}.local`],r={};for(let e of n)Object.assign(r,V((0,a.join)(t,e)));return r}function V(e){if(!(0,t.existsSync)(e))return{};let n={};for(let r of(0,t.readFileSync)(e,`utf-8`).split(/\r?\n/)){let e=r.trim();if(!e||e.startsWith(`#`))continue;let t=e.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);if(!t)continue;let i=t[2].trim(),a=i[0],o=a===`"`||a===`'`||a==="`"?i.indexOf(a,1):-1;n[t[1]]=o===-1?i.replace(/\s+#.*$/,``).trim():i.slice(1,o)}return n}exports.PrimitiveEnvError=_,exports.primitiveDevTools=d,exports.primitiveEnv=z,exports.resolvePrimitiveEnv=N;
68
+ `},transformIndexHtml(e){if(!h||!d)return e;let t=`<style data-primitive-devtools-sonner>${g}</style>`;return e.replace(`</body>`,`${t}\n<script type="module" src="/@id/__x00__virtual:primitive-devtools-init"><\/script>\n</body>`)},hotUpdate({file:e,server:t}){if(e.endsWith(`.primitive-test.ts`)){let e=t.moduleGraph.getModuleById(c);e&&(t.moduleGraph.invalidateModule(e),t.ws.send({type:`full-reload`}))}}}}var f=`primitive`,p=`.primitive`,m=`config.json`,h=`local.json`,g=`primitive/config.json`,_=`.primitive/local.json`;function v(e,t){return`Found ${e} but no ${t}: this project still uses the retired layout under ${p}/, which is never read.`}function y(e){return`No ${g} found in ${e} or any parent directory. Primitive commands run inside a Primitive project: it is where the CLI reads the environment, the app that environment names, and the configuration tree from. Run 'primitive init' to create one.`}var b=class extends Error{kind;path;constructor(e,t,n){super(t),this.name=`PrimitiveEnvError`,this.kind=e,this.path=n}};function x(e){if(typeof e!=`string`)return;let t=e.trim();if(!t)return;let n;try{n=new URL(t)}catch{return}if(!(n.protocol!==`https:`&&n.protocol!==`http:`)&&!(n.protocol===`http:`&&n.hostname!==`localhost`&&n.hostname!==`127.0.0.1`)&&!(n.username||n.password)&&!(n.pathname!==`/`&&n.pathname!==``)&&!(n.search||n.hash))return n.origin}function S(e,t){return`Environment "${e}" in ${t?`${g} (${t})`:g} has no "appId". Every environment names exactly one app — add "appId": "<app-id>" to that environment in ${g}. Find the app id in the Primitive admin UI, with 'primitive apps list' run from another Primitive project, or as the residual "currentAppId" in .primitive/credentials.json if this environment was ever pointed at an app by the retired per-machine app selection.`}function C(e){if(typeof e==`string`)return e.trim()||void 0}function w(e){return e.startsWith(`https://`)?`wss://`+e.slice(8):e.startsWith(`http://`)?`ws://`+e.slice(7):e}function T(e={}){let n=(e.env??process.env).PRIMITIVE_PROJECT_CONFIG;if(n){let e=(0,a.resolve)(n);if((0,a.basename)((0,a.dirname)(e))===`.primitive`)throw new b(`stale-layout`,v(e,(0,a.join)((0,a.dirname)((0,a.dirname)(e)),f,m)),e);return(0,t.existsSync)(e)?e:null}let r=(0,a.resolve)(e.cwd??process.cwd());for(;;){let e=(0,a.join)(r,f,m);if((0,t.existsSync)(e))return e;let n=(0,a.join)(r,p,m);if((0,t.existsSync)(n))throw new b(`stale-layout`,v(n,e),n);let i=(0,a.dirname)(r);if(i===r)return null;r=i}}function E(e){let t=(0,a.dirname)(e);return(0,a.basename)(t)===`primitive`?(0,a.dirname)(t):t}function D(e){return(0,a.join)(E(e),p,h)}function O(e){return e instanceof Error?e.message:String(e)}function k(e){if(!(0,t.existsSync)(e))throw new b(`missing-config`,y((0,a.dirname)(e)),e);let n;try{n=(0,t.readFileSync)(e,`utf-8`)}catch(t){throw new b(`malformed-config`,`Failed to read ${g} (${e}): ${O(t)}`,e)}let r;try{r=JSON.parse(n)}catch(t){throw new b(`malformed-config`,`Failed to parse ${g} (${e}): ${O(t)}`,e)}if(!r||typeof r!=`object`||Array.isArray(r))throw new b(`malformed-config`,`${g} (${e}) must contain a JSON object at the top level.`,e);let i=r;if(typeof i.version!=`number`||!Number.isInteger(i.version))throw new b(`malformed-config`,`${g} (${e}) is missing required integer field "version".`,e);if(i.version!==1)throw new b(`unsupported-version`,`${g} (${e}) has version ${i.version}, but this tool understands version 1.`,e);if(!i.environments||typeof i.environments!=`object`||Array.isArray(i.environments))throw new b(`malformed-config`,`${g} (${e}) is missing required "environments" object.`,e);let o={};for(let[t,n]of Object.entries(i.environments)){if(!n||typeof n!=`object`||Array.isArray(n))throw new b(`malformed-config`,`Environment "${t}" must be an object in ${g} (${e}).`,e);let r=n;if(typeof r.apiUrl!=`string`||!r.apiUrl)throw new b(`malformed-config`,`Environment "${t}" must have a non-empty "apiUrl" string in ${g} (${e}).`,e);let i=C(r.appId);if(!i)throw new b(`missing-app-id`,S(t,e),e);o[t]={apiUrl:r.apiUrl.replace(/\/$/,``),appId:i,appName:typeof r.appName==`string`?r.appName:void 0,webUrl:x(r.webUrl),description:typeof r.description==`string`?r.description:void 0}}return{version:i.version,defaultEnvironment:typeof i.defaultEnvironment==`string`&&i.defaultEnvironment?i.defaultEnvironment:void 0,environments:o}}var A=/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;function j(e,n){let r=D(e);if(!(0,t.existsSync)(r))return{selectedEnvironment:null,environments:{}};let i=e=>{throw new b(`corrupt-local-state`,`${_} (${r}) is unreadable: ${e}. Delete the file or re-run 'primitive env use <name>'.`,r)},a;try{a=JSON.parse((0,t.readFileSync)(r,`utf-8`))}catch(e){return i(O(e))}if(!a||typeof a!=`object`||Array.isArray(a))return i(`expected a JSON object`);let o=a,s=null,c=o.selectedEnvironment;if(c!=null&&c!==``){if(typeof c!=`string`)return i(`"selectedEnvironment" must be a string`);s=c}return{selectedEnvironment:s,environments:M(o.environments,r,n)}}function M(e,t,n){let r={};if(e===void 0)return r;if(!e||typeof e!=`object`||Array.isArray(e))throw new b(`corrupt-local-state`,`${_} (${t}) is unreadable: "environments" must be an object keyed by environment name. Fix or remove "environments" in ${_}.`,t);let i=(e,n)=>{throw new b(`corrupt-local-state`,`${_} (${t}): machine-local environment "${e}" ${n}. Fix or remove the "${e}" entry under "environments" in ${_}.`,t)},a=n?Object.keys(n.environments):null;for(let[t,n]of Object.entries(e)){A.test(t)||i(t,`is not a valid environment name (letters, digits, dashes and underscores, starting with a letter or digit)`),(!n||typeof n!=`object`||Array.isArray(n))&&i(t,`must be an object`);let e=n;(typeof e.apiUrl!=`string`||!e.apiUrl.trim())&&i(t,`has no "apiUrl" string`);let o=C(e.appId);o||i(t,`has no "appId" naming the app it selects`),(typeof e.tree!=`string`||!e.tree.trim())&&i(t,`has no "tree" naming the committed environment whose configuration tree it uses`);let s=e.tree.trim();a&&(a.includes(t)&&i(t,`has the name of a committed environment in ${g}; a machine-local environment needs a name of its own`),a.includes(s)||i(t,`has "tree": "${s}", which is not an environment in ${g} (available: ${a.join(`, `)||`(none)`})`)),r[t]={apiUrl:e.apiUrl.trim().replace(/\/$/,``),appId:o,appName:typeof e.appName==`string`?e.appName:void 0,tree:s}}return r}function N(e,t){return Object.prototype.hasOwnProperty.call(e.environments,t)}function P(e,t){let n=Object.keys(e.environments),r=Object.keys(t.localEnvironments??{}),i=[...n,...r.map(e=>`${e} (local)`)].join(`, `)||`(none)`,a=t.configPath?`${g} (${t.configPath})`:g,o=(n,o,s,c=!1)=>{if(!N(e,n)&&(c||!r.includes(n)))throw new b(`unknown-environment`,`Environment "${n}" is not defined in ${a} (selected ${s}). Available: ${i}`,t.configPath);return{name:n,source:o}};if(t.explicitEnvName)return o(t.explicitEnvName,`explicit`,`explicitly`);if(t.envVarName)return o(t.envVarName,`env-var`,`via PRIMITIVE_ENV`);if(t.localSelection)return o(t.localSelection,`local`,`in ${_}`);if(e.defaultEnvironment)return o(e.defaultEnvironment,`default`,`as "defaultEnvironment"`,!0);if(n.length===1)return{name:n[0],source:`sole`};throw n.length===0?new b(`no-selection`,`${a} has no environments defined. Run 'primitive env add <name>' or 'primitive init'.`,t.configPath):new b(`no-selection`,`No environment selected. Run 'primitive env use <name>', set "defaultEnvironment" in ${a}, or export PRIMITIVE_ENV. Available: ${i}`,t.configPath)}function F(e={}){let t=e.env??process.env,n=e.configPath??T({cwd:e.cwd,env:t});if(!n)throw new b(`missing-config`,y(e.cwd??process.cwd()));let r=E(n),i=k(n),a=j(n,i),{name:o,source:s}=P(i,{explicitEnvName:e.explicitEnvName||null,envVarName:t.PRIMITIVE_ENV||null,localSelection:a.selectedEnvironment,localEnvironments:a.environments,configPath:n}),c={name:o,source:s,configPath:n,localStatePath:D(n),projectRoot:r},l=N(i,o)?void 0:a.environments[o];if(l)return{...c,apiUrl:l.apiUrl,wsUrl:w(l.apiUrl),appId:l.appId,appName:l.appName,webUrl:void 0,description:void 0,tree:l.tree,local:!0};let u=i.environments[o];return{...c,apiUrl:u.apiUrl,wsUrl:w(u.apiUrl),appId:u.appId,appName:u.appName,webUrl:u.webUrl,description:u.description,tree:o,local:!1}}var I=[`VITE_APP_ID`,`VITE_API_URL`,`VITE_WS_URL`,`VITE_APP_NAME`],L=`VITE_EXPECTED_PRIMITIVE_ENV`,R=new Map;function z(e,t){let n=process.env[e];n!==void 0&&n!==``&&R.get(e)!==n||(process.env[e]=t,R.set(e,t))}function B(e={}){return F({cwd:e.root,env:e.env,explicitEnvName:e.primitiveEnv??null})}function V(e,t,n){let r=e=>n.includes(e)?` (overridden)`:``,i=[`[primitive-env] Primitive environment: ${e.name} (selected: ${e.source})`,`[primitive-env] apiUrl: ${t.apiUrl}${r(`VITE_API_URL`)}`,`[primitive-env] wsUrl: ${t.wsUrl}${r(`VITE_WS_URL`)}`,`[primitive-env] appId: ${t.appId??`(unset)`}${r(`VITE_APP_ID`)}`,`[primitive-env] appName: ${t.appName??`(unset)`}${r(`VITE_APP_NAME`)}`,`[primitive-env] config: ${e.configPath}`];return n.length>0&&i.push(`[primitive-env] overridden by the environment: ${n.join(`, `)}`),i.join(`
69
+ `)}function H(e,t){let n=e?.envDir;return n===!1?null:typeof n==`string`&&n!==``?(0,a.resolve)(t,n):t}function U(e,t){let n=process.env[L];if(n!==void 0&&n!==``)return{value:n,where:`the process environment`};if(t===null)return null;for(let n of[`.env.${e}.local`,`.env.${e}`,`.env.local`,`.env`]){let e=J((0,a.join)(t,n))[L];if(e!==void 0)return{value:e,where:n}}return null}function W(e,t,n){let r=n.local?`"${n.name}" (machine-local, tree "${n.tree}")`:`"${n.name}"`;return[`[primitive-env] Wrong pair: vite mode "${e}" is declared to run against Primitive environment "${t.value}", but ${r} resolved.`,`[primitive-env] expected: ${t.value} (${L} in ${t.where}; a machine-local environment whose tree is ${t.value} also pairs)`,`[primitive-env] resolved: ${n.name} (selected: ${n.source}${n.local?`; machine-local, tree ${n.tree}`:``})`,`[primitive-env] config: ${n.configPath}`,``,`Run the pair you meant — PRIMITIVE_ENV=${t.value} <command> --mode ${e}, or 'primitive env use ${t.value}' — or change/remove ${L} in ${t.where}.`,``,`A mode declares this when its .env.${e} carries values coupled to one backend, so continuing would run this environment's identity with another's configuration.`].join(`
70
+ `)}function G(e,t,n){let r=U(e,t);if(r&&r.value!==``&&r.value!==n.name&&!(n.local&&r.value===n.tree))throw Error(W(e,r,n))}function K(e={}){let t=null,n=null,r,i=[],a=!1,o=null;return{name:`primitive-env`,enforce:`pre`,config(s,c){a=c?.command===`serve`,o=null;let l=s?.root??process.cwd(),u=c?.mode??`development`,d=q(u,l),f={};for(let e of I){let t=process.env[e],n=t!==void 0&&R.get(e)!==t?t:d[e];n!==void 0&&n!==``&&(f[e]=n)}let p;try{p=F({cwd:l,explicitEnvName:e.primitiveEnv??null})}catch(e){if(e instanceof b&&e.kind===`missing-config`&&f.VITE_APP_ID&&f.VITE_API_URL)return console.warn(`[primitive-env] No primitive/config.json found; using VITE_APP_ID / VITE_API_URL from the environment. Run 'primitive init' to make the project the source of truth.`),r=f.VITE_APP_NAME,{};throw e}if(!(f.VITE_APP_ID&&f.VITE_API_URL)){let e=H(s,l);G(u,e,p),o={root:l,mode:u,dir:e}}t=p,r=f.VITE_APP_NAME??p.appName;let m={VITE_APP_ID:p.appId,VITE_API_URL:p.apiUrl,VITE_WS_URL:p.wsUrl,VITE_APP_NAME:p.appName},h={"import.meta.env.VITE_PRIMITIVE_ENV":JSON.stringify(p.name)},g=[];for(let e of I){if(f[e]!==void 0){g.push(e);continue}let t=m[e];t!==void 0&&(h[`import.meta.env.${e}`]=JSON.stringify(t),z(e,t))}return z(`VITE_PRIMITIVE_ENV`,p.name),i=g,n={apiUrl:f.VITE_API_URL??p.apiUrl,wsUrl:f.VITE_WS_URL??p.wsUrl,appId:f.VITE_APP_ID??p.appId,appName:r},e.logResolved!==!1&&console.log(V(p,n,g)),g.length>0&&console.warn(`[primitive-env] Overridden by an explicit value in .env or the shell: ${g.join(`, `)}. Remove it to use primitive/config.json (deploys reject these outright).`),{define:h}},configResolved(e){if(!o||!t)return;let n=H(e,o.root),r=e?.mode??o.mode;n===o.dir&&r===o.mode||(G(r,n,t),o={root:o.root,mode:r,dir:n})},transformIndexHtml(){if(!a||!t||!n)return[];let e=`%c[primitive] env=${t.name} api=${n.apiUrl} app=${n.appId??`(unset)`} (${t.configPath})`+(i.length>0?` overridden: ${i.join(`, `)}`:``);return[{tag:`script`,injectTo:`head`,children:`console.info(${JSON.stringify(e)}, "color:#888");`}]},appName(){return r},resolved(){return t}}}function q(e,t){let n=[`.env`,`.env.local`,`.env.${e}`,`.env.${e}.local`],r={};for(let e of n)Object.assign(r,J((0,a.join)(t,e)));return r}function J(e){if(!(0,t.existsSync)(e))return{};let n={};for(let r of(0,t.readFileSync)(e,`utf-8`).split(/\r?\n/)){let e=r.trim();if(!e||e.startsWith(`#`))continue;let t=e.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);if(!t)continue;let i=t[2].trim(),a=i[0],o=a===`"`||a===`'`||a==="`"?i.indexOf(a,1):-1;n[t[1]]=o===-1?i.replace(/\s+#.*$/,``).trim():i.slice(1,o)}return n}exports.PrimitiveEnvError=b,exports.primitiveDevTools=d,exports.primitiveEnv=K,exports.resolvePrimitiveEnv=B;