sitevision-cli 1.0.0-beta.2 → 1.0.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/app.d.ts +1 -1
  2. package/dist/app.js +59 -8
  3. package/dist/cli.js +96 -39
  4. package/dist/commands/build.js +1 -1
  5. package/dist/commands/deploy.d.ts +2 -2
  6. package/dist/commands/deploy.js +135 -25
  7. package/dist/commands/dev.d.ts +8 -10
  8. package/dist/commands/dev.js +77 -366
  9. package/dist/commands/info.js +2 -2
  10. package/dist/commands/watch.js +5 -23
  11. package/dist/components/AnimatedLogo.js +8 -2
  12. package/dist/components/AuthLoginScreen.d.ts +21 -0
  13. package/dist/components/AuthLoginScreen.js +90 -0
  14. package/dist/components/DevPropertiesForm.d.ts +2 -1
  15. package/dist/components/DevPropertiesForm.js +198 -33
  16. package/dist/components/InfoScreen.js +2 -2
  17. package/dist/components/MainMenu.js +7 -2
  18. package/dist/components/PasswordInput.js +2 -1
  19. package/dist/components/SetupFlow.d.ts +2 -1
  20. package/dist/components/SetupFlow.js +100 -11
  21. package/dist/shell/AddonPicker.d.ts +14 -0
  22. package/dist/shell/AddonPicker.js +54 -0
  23. package/dist/shell/CommandPalette.d.ts +8 -0
  24. package/dist/shell/CommandPalette.js +63 -0
  25. package/dist/shell/ConfigForm.d.ts +36 -0
  26. package/dist/shell/ConfigForm.js +558 -0
  27. package/dist/shell/Frame.d.ts +59 -0
  28. package/dist/shell/Frame.js +134 -0
  29. package/dist/shell/Settings.d.ts +6 -0
  30. package/dist/shell/Settings.js +96 -0
  31. package/dist/shell/Shell.d.ts +9 -0
  32. package/dist/shell/Shell.js +586 -0
  33. package/dist/shell/Tabs.d.ts +36 -0
  34. package/dist/shell/Tabs.js +90 -0
  35. package/dist/shell/actions.d.ts +45 -0
  36. package/dist/shell/actions.js +0 -0
  37. package/dist/types/index.d.ts +44 -5
  38. package/dist/utils/config.d.ts +10 -0
  39. package/dist/utils/config.js +14 -0
  40. package/dist/utils/environments.d.ts +20 -0
  41. package/dist/utils/environments.js +74 -0
  42. package/dist/utils/i18n.d.ts +12 -0
  43. package/dist/utils/i18n.js +279 -0
  44. package/dist/utils/jsonc.d.ts +19 -0
  45. package/dist/utils/jsonc.js +74 -0
  46. package/dist/utils/keychain.d.ts +9 -0
  47. package/dist/utils/keychain.js +54 -0
  48. package/dist/utils/oauth2-auth.d.ts +64 -0
  49. package/dist/utils/oauth2-auth.js +242 -0
  50. package/dist/utils/password-prompt.d.ts +5 -0
  51. package/dist/utils/password-prompt.js +28 -0
  52. package/dist/utils/project-detection.d.ts +105 -6
  53. package/dist/utils/project-detection.js +411 -54
  54. package/dist/utils/session-cookie-auth.d.ts +35 -0
  55. package/dist/utils/session-cookie-auth.js +99 -0
  56. package/dist/utils/sitevision-api.d.ts +64 -5
  57. package/dist/utils/sitevision-api.js +195 -33
  58. package/dist/utils/tasks.d.ts +48 -0
  59. package/dist/utils/tasks.js +371 -0
  60. package/dist/utils/workspace.d.ts +17 -0
  61. package/dist/utils/workspace.js +67 -0
  62. package/package.json +3 -1
  63. package/readme.md +102 -121
@@ -0,0 +1,90 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import Spinner from 'ink-spinner';
4
+ import { appTypeOf, getPackageJsonSyncChanges, readAncestorDevProperties, localizedText, } from '../utils/project-detection.js';
5
+ import { checkSitevisionScriptsCompatibility } from '../utils/sitevision-scripts-runner.js';
6
+ import { ACCENT, elapsed } from './Frame.js';
7
+ import { t } from '../utils/i18n.js';
8
+ export const TABS = [
9
+ { id: 'overview', label: 'Overview', short: 'Ovw' },
10
+ { id: 'config', label: 'Config', short: 'Cfg' },
11
+ { id: 'versions', label: 'Versions', short: 'Ver' },
12
+ { id: 'log', label: 'Log', short: 'Log' },
13
+ ];
14
+ export function TabBar({ tab, narrow, focused, }) {
15
+ return (_jsx(Box, { paddingX: 1, children: TABS.map((entry, i) => (_jsxs(Text, { children: [_jsxs(Text, { bold: entry.id === tab, color: entry.id === tab ? ACCENT : undefined, dimColor: entry.id !== tab, underline: entry.id === tab && focused, children: [i + 1, " ", t(narrow ? entry.short : entry.label)] }), ' '.repeat(3)] }, entry.id))) }));
16
+ }
17
+ function Row({ label, value, dim, }) {
18
+ return (_jsx(Box, { height: 1, flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { dimColor: true, children: label.padEnd(14) }), value, dim && _jsxs(Text, { dimColor: true, children: [" ", dim] })] }) }));
19
+ }
20
+ const LEVEL_COLOR = {
21
+ info: undefined,
22
+ ok: 'green',
23
+ warn: 'yellow',
24
+ error: 'red',
25
+ };
26
+ const STATUS_GLYPH = {
27
+ running: _jsx(Spinner, { type: "dots" }),
28
+ success: _jsx(Text, { color: "green", children: "\u2713" }),
29
+ error: _jsx(Text, { color: "red", children: "\u2717" }),
30
+ stopped: _jsx(Text, { dimColor: true, children: "\u25A0" }),
31
+ };
32
+ function time(ms, seconds = false) {
33
+ const d = new Date(ms);
34
+ const hh = String(d.getHours()).padStart(2, '0');
35
+ const mm = String(d.getMinutes()).padStart(2, '0');
36
+ const ss = String(d.getSeconds()).padStart(2, '0');
37
+ return seconds ? `${hh}:${mm}:${ss}` : `${hh}:${mm}`;
38
+ }
39
+ export function Overview({ project, tasks, height, }) {
40
+ const dev = project.devProperties;
41
+ const inherited = new Set(project.inheritedKeys);
42
+ const ancestors = readAncestorDevProperties(project.root);
43
+ const src = (key) => inherited.has(key)
44
+ ? Object.hasOwn(ancestors, key)
45
+ ? t('↑ root')
46
+ : 'package.json'
47
+ : undefined;
48
+ const notSet = t('not set');
49
+ const sync = getPackageJsonSyncChanges(project.root).length;
50
+ const scripts = checkSitevisionScriptsCompatibility(project.root);
51
+ const recent = tasks
52
+ .filter(task => task.appRoot === project.root && task.status !== 'running')
53
+ .slice(-Math.max(1, height - 18))
54
+ .toReversed();
55
+ const status = (ok, okText, badText, warn = false) => (_jsxs(Text, { children: [_jsx(Text, { color: ok ? 'green' : warn ? 'yellow' : 'red', children: ok ? '✓' : warn ? '~' : '✗' }), ' ', ok ? okText : badText] }));
56
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [_jsx(Box, { height: 1, flexShrink: 0, children: _jsxs(Text, { bold: true, wrap: "truncate", children: [localizedText(project.manifest.name) || project.manifest.id, ' ', _jsxs(Text, { dimColor: true, children: [project.manifest.type, project.manifest.bundled ? t(' · bundled') : ''] })] }) }), _jsx(Row, { label: t('id'), value: project.manifest.id }), _jsx(Row, { label: t('version'), value: project.manifest.version }), _jsx(Row, { label: t('type'), value: `${project.manifest.type} (${appTypeOf(project.manifest) ?? '?'})` }), _jsx(Row, { label: t('addon'), value: dev?.addonName ?? notSet, dim: src('addonName') }), _jsx(Row, { label: t('site'), value: dev?.siteName ?? notSet, dim: src('siteName') }), _jsx(Row, { label: t('environment'), value: dev?.environmentName ?? 'dev', dim: dev?.productionEnvironment ? t('production') : undefined }), _jsx(Row, { label: t('domain'), value: dev?.domain ?? notSet, dim: src('domain') }), _jsx(Row, { label: t('auth'), value: dev?.authMethod ?? (dev ? 'basic' : notSet), dim: src('authMethod') }), _jsx(Row, { label: t('signing user'), value: dev?.signingUsername ?? notSet, dim: src('signingUsername') }), _jsxs(Box, { marginTop: 1, flexDirection: "column", flexShrink: 0, children: [_jsxs(Text, { dimColor: true, children: [t('deps').padEnd(10), status(project.hasNodeModules, 'node_modules', t('missing · i to install'))] }), _jsxs(Text, { dimColor: true, children: [t('config').padEnd(10), status(Boolean(dev), t('dev properties'), t('missing · e to edit'))] }), _jsxs(Text, { dimColor: true, children: [t('sync').padEnd(10), status(sync === 0, 'package.json', sync === 1
57
+ ? t('1 diff · y to apply')
58
+ : t('{n} diffs · y to apply', { n: sync }), true)] }), _jsxs(Text, { dimColor: true, children: [t('signing').padEnd(10), status(project.hasSigningProperties, dev?.signingUsername ?? '', t('missing · / set up signing'))] }), _jsxs(Text, { dimColor: true, children: [t('scripts').padEnd(10), status(scripts.status === 'ok', scripts.installed ?? '', scripts.installed
59
+ ? `${scripts.installed} · ${scripts.status}`
60
+ : t('not installed · i to install'), scripts.status !== 'not-installed')] }), project.hasLegacyPassword && (_jsxs(Text, { color: "yellow", children: [' '.repeat(10), t('⚠ plaintext password in .dev_properties.json · / migrate')] }))] }), recent.length > 0 && (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, dimColor: true, children: t('RECENT') }), recent.map(task => (_jsxs(Text, { wrap: "truncate", children: [_jsxs(Text, { dimColor: true, children: [time(task.endedAt ?? task.startedAt), " "] }), STATUS_GLYPH[task.status], " ", task.label, ' ', _jsx(Text, { dimColor: true, children: task.error ?? elapsed(task) })] }, task.id)))] }))] }));
61
+ }
62
+ export function Versions({ project, state, selected, }) {
63
+ if (!project.devProperties) {
64
+ return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { color: "yellow", children: t('⚠ Configure dev properties first (e).') }) }));
65
+ }
66
+ if (!state) {
67
+ return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: t('Press R to fetch versions from {domain}.', {
68
+ domain: project.devProperties.domain,
69
+ }) }) }));
70
+ }
71
+ const list = state.executables ?? [];
72
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [_jsxs(Text, { dimColor: true, children: [t('APP IDENTIFIER').padEnd(30), t('VERSION').padEnd(12), t('ACTIVE'), state.loading && (_jsxs(Text, { color: ACCENT, children: [' '.repeat(3), _jsx(Spinner, { type: "dots" })] }))] }), state.error && _jsx(Text, { color: "red", children: state.error }), list.map((e, i) => (_jsxs(Text, { backgroundColor: i === selected ? ACCENT : undefined, color: i === selected ? 'black' : undefined, wrap: "truncate", children: [e.appIdentifier.padEnd(30).slice(0, 30), e.appVersion.padEnd(12), e.active ? '●' : '○', i === selected ? ` ${e.id}` : ''] }, e.id))), !state.loading && !state.error && list.length === 0 && (_jsx(Text, { dimColor: true, children: t('No versions uploaded to {addon}.', {
73
+ addon: project.devProperties.addonName,
74
+ }) })), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: [list.length === 1
75
+ ? t('1 version · a activate selected · R refresh')
76
+ : t('{n} versions · a activate selected · R refresh', {
77
+ n: list.length,
78
+ }), state.fetchedAt
79
+ ? t(' · fetched {time}', { time: time(state.fetchedAt) })
80
+ : ''] }) })] }));
81
+ }
82
+ export function Log({ task, height, scroll, wrap, }) {
83
+ if (!task) {
84
+ return (_jsx(Box, { paddingX: 1, children: _jsx(Text, { dimColor: true, children: t('No task yet. d dev · w watch · b build · s sign · p deploy') }) }));
85
+ }
86
+ const visible = Math.max(1, height - 2);
87
+ const end = Math.max(0, task.lines.length - scroll);
88
+ const lines = task.lines.slice(Math.max(0, end - visible), end);
89
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, overflow: "hidden", children: [_jsxs(Text, { children: [STATUS_GLYPH[task.status], " ", task.label, " ", task.appName, ' ', _jsxs(Text, { dimColor: true, children: [task.phase, " \u00B7 ", elapsed(task), scroll > 0 ? ` · ↑${scroll}` : t(' · following')] })] }), lines.map((line, i) => (_jsxs(Text, { wrap: wrap ? 'wrap' : 'truncate', color: LEVEL_COLOR[line.level], children: [_jsxs(Text, { dimColor: true, children: [time(line.time, true), " ", line.tag.padEnd(3)] }), ' ', line.text] }, i)))] }));
90
+ }
@@ -0,0 +1,45 @@
1
+ import type { ProjectInfo, DeployConfig } from '../types/index.js';
2
+ export type Tab = 'overview' | 'config' | 'versions' | 'log';
3
+ export type Credential = {
4
+ accessToken?: string;
5
+ sessionCookie?: string;
6
+ };
7
+ export interface ActionContext {
8
+ project: ProjectInfo;
9
+ reload: () => void;
10
+ askPassword: (label: string, rememberLabel?: string) => Promise<{
11
+ password: string;
12
+ remember: boolean;
13
+ } | null>;
14
+ login: (method: 'oauth2' | 'cookie') => Promise<Credential | null>;
15
+ confirm: (message: string) => Promise<boolean>;
16
+ setTab: (tab: Tab) => void;
17
+ openWorkspaceSettings?: () => void;
18
+ openSettings: () => void;
19
+ environment: string;
20
+ isProduction: boolean;
21
+ cycleEnvironment: () => void;
22
+ addEnvironment: () => Promise<void>;
23
+ notify: (text: string, level?: 'info' | 'ok' | 'warn' | 'error') => void;
24
+ quit: () => void;
25
+ }
26
+ export interface Action {
27
+ id: string;
28
+ key?: string;
29
+ group: 'app' | 'setup' | 'auth';
30
+ label: string;
31
+ detail?: (project: ProjectInfo) => string | undefined;
32
+ enabled?: (project: ProjectInfo) => boolean;
33
+ run: (ctx: ActionContext) => Promise<void>;
34
+ }
35
+ export declare function forgetSession(project: ProjectInfo): void;
36
+ /** Top-bar auth state without prompting for anything. */
37
+ export declare function authState(project: ProjectInfo): {
38
+ ready: boolean;
39
+ label: string;
40
+ };
41
+ export declare function resolveDeployConfig(ctx: ActionContext, fresh?: boolean): Promise<DeployConfig | null>;
42
+ export declare const actions: Action[];
43
+ export declare function actionForKey(key: string): Action | undefined;
44
+ /** Subsequence match, case-insensitive: "dpp" matches "Deploy to production". */
45
+ export declare function fuzzyMatch(query: string, text: string): boolean;
Binary file
@@ -6,21 +6,27 @@
6
6
  /**
7
7
  * App types supported by Sitevision
8
8
  */
9
- export type AppType = 'WebApp' | 'Widget' | 'RESTApp';
9
+ export type AppType = 'WebApp' | 'Widget' | 'RESTApp' | 'MCPServer';
10
10
  /**
11
11
  * Simplified app type for internal use
12
12
  */
13
- export type SimpleAppType = 'web' | 'widget' | 'rest';
13
+ export type SimpleAppType = 'web' | 'widget' | 'rest' | 'mcp';
14
+ /**
15
+ * A manifest text field that may be a plain string or a localized object keyed
16
+ * by language code, e.g. `{sv: 'Namn', en: 'Name'}`. Sitevision allows either
17
+ * form for human-facing fields like `name` and `description`.
18
+ */
19
+ export type LocalizedString = string | Record<string, string>;
14
20
  /**
15
21
  * Sitevision app manifest (manifest.json)
16
22
  */
17
23
  export interface SitevisionManifest {
18
24
  id: string;
19
- name: string;
25
+ name: LocalizedString;
20
26
  version: string;
21
27
  type: AppType;
22
28
  bundled?: boolean;
23
- description?: string;
29
+ description?: LocalizedString;
24
30
  author?: string;
25
31
  helpUrl?: string;
26
32
  license?: string;
@@ -42,6 +48,33 @@ export interface DevProperties {
42
48
  useHTTPForDevDeploy?: boolean;
43
49
  signingUsername?: string;
44
50
  certificateName?: string;
51
+ authMethod?: 'basic' | 'oauth2' | 'cookie';
52
+ oauth2?: OAuth2Config;
53
+ sessionLoginUrl?: string;
54
+ baseEnvironment?: string;
55
+ production?: boolean;
56
+ environments?: Record<string, EnvironmentOverride>;
57
+ accessToken?: string;
58
+ sessionCookie?: string;
59
+ environmentName?: string;
60
+ productionEnvironment?: boolean;
61
+ }
62
+ /** Fields an environment may override; everything else comes from the base. */
63
+ export type EnvironmentOverride = Partial<Pick<DevProperties, 'domain' | 'siteName' | 'addonName' | 'username' | 'authMethod' | 'oauth2' | 'sessionLoginUrl' | 'useHTTPForDevDeploy'>> & {
64
+ production?: boolean;
65
+ };
66
+ /**
67
+ * OAuth2 provider config for bearer-token deploys.
68
+ *
69
+ * Deliberately secret-free: the client secret and refresh token live in the OS
70
+ * keychain, so nothing here is unsafe to write to .dev_properties.json.
71
+ */
72
+ export interface OAuth2Config {
73
+ authorizationEndpoint: string;
74
+ tokenEndpoint: string;
75
+ clientId: string;
76
+ scopes?: string[];
77
+ redirectPort?: number;
45
78
  }
46
79
  /**
47
80
  * Signing credentials (password is runtime-only, not persisted)
@@ -59,7 +92,9 @@ export interface DeployConfig {
59
92
  siteName: string;
60
93
  addonName: string;
61
94
  username: string;
62
- password: string;
95
+ password?: string;
96
+ accessToken?: string;
97
+ sessionCookie?: string;
63
98
  useHTTP?: boolean;
64
99
  }
65
100
  /**
@@ -86,6 +121,7 @@ export interface ProjectInfo {
86
121
  hasSigningProperties: boolean;
87
122
  hasLegacyPassword: boolean;
88
123
  devProperties?: DevProperties;
124
+ inheritedKeys: string[];
89
125
  packageJson: PackageJson;
90
126
  hasSitevisionScripts: boolean;
91
127
  hasNodeModules: boolean;
@@ -143,6 +179,7 @@ export interface DeployResponse {
143
179
  executableId?: string;
144
180
  message?: string;
145
181
  error?: string;
182
+ authExpired?: boolean;
146
183
  }
147
184
  /**
148
185
  * API response from addon creation
@@ -151,6 +188,7 @@ export interface CreateAddonResponse {
151
188
  success: boolean;
152
189
  addonId?: string;
153
190
  error?: string;
191
+ authExpired?: boolean;
154
192
  }
155
193
  /**
156
194
  * API response from activation
@@ -158,6 +196,7 @@ export interface CreateAddonResponse {
158
196
  export interface ActivationResponse {
159
197
  success: boolean;
160
198
  error?: string;
199
+ authExpired?: boolean;
161
200
  }
162
201
  /**
163
202
  * Build mode
@@ -1,3 +1,13 @@
1
+ import { type Language } from './i18n.js';
2
+ /** User-facing preferences editable from the shell's Settings screen. */
3
+ export interface Settings {
4
+ language: Language;
5
+ introAnimation: boolean;
6
+ }
7
+ export declare function getSettings(): Settings;
8
+ export declare function setSettings(patch: Partial<Settings>): void;
9
+ /** Path of the config file, for display. */
10
+ export declare function settingsFile(): string;
1
11
  /**
2
12
  * True until the user has completed the first-run welcome at least once.
3
13
  */
@@ -26,6 +26,20 @@ function writeConfig(config) {
26
26
  // shows again next time, which is harmless.
27
27
  }
28
28
  }
29
+ export function getSettings() {
30
+ const config = readConfig();
31
+ return {
32
+ language: config.language ?? 'en',
33
+ introAnimation: config.introAnimation ?? true,
34
+ };
35
+ }
36
+ export function setSettings(patch) {
37
+ writeConfig({ ...readConfig(), ...patch });
38
+ }
39
+ /** Path of the config file, for display. */
40
+ export function settingsFile() {
41
+ return configFile();
42
+ }
29
43
  /**
30
44
  * True until the user has completed the first-run welcome at least once.
31
45
  */
@@ -0,0 +1,20 @@
1
+ import type { DevProperties, EnvironmentOverride, ProjectInfo } from '../types/index.js';
2
+ /** Name of the environment the top-level dev properties describe. */
3
+ export declare function baseEnvironment(dev?: Partial<DevProperties>): string;
4
+ export declare function environmentNames(dev?: Partial<DevProperties>): string[];
5
+ export declare function isProductionEnvironment(name: string, dev?: Partial<DevProperties>): boolean;
6
+ /** Badge colour: dev green, production red, anything else yellow. */
7
+ export declare function environmentColor(name: string, dev?: Partial<DevProperties>): 'green' | 'yellow' | 'red';
8
+ /**
9
+ * The effective dev properties for one environment: the base with that
10
+ * environment's overrides applied and credentials resolved for its
11
+ * domain/username. The base ("dev") is returned as-is.
12
+ */
13
+ export declare function resolveEnvironment(dev: DevProperties, name: string): DevProperties;
14
+ /** A project view whose dev properties are resolved for `name`. */
15
+ export declare function environmentProject(project: ProjectInfo, name: string): ProjectInfo;
16
+ /**
17
+ * Write one environment's override into the base: keep only the keys whose
18
+ * value differs from the base, so the file stays minimal.
19
+ */
20
+ export declare function withEnvironmentOverride(base: DevProperties, name: string, values: EnvironmentOverride): DevProperties;
@@ -0,0 +1,74 @@
1
+ import { resolveRuntimeSecrets } from './project-detection.js';
2
+ /** Name of the environment the top-level dev properties describe. */
3
+ export function baseEnvironment(dev) {
4
+ return dev?.baseEnvironment?.trim() || 'dev';
5
+ }
6
+ export function environmentNames(dev) {
7
+ const base = baseEnvironment(dev);
8
+ return [
9
+ base,
10
+ ...Object.keys(dev?.environments ?? {}).filter(name => name !== base),
11
+ ];
12
+ }
13
+ export function isProductionEnvironment(name, dev) {
14
+ if (name === baseEnvironment(dev))
15
+ return dev?.production ?? false;
16
+ return dev?.environments?.[name]?.production ?? /prod/i.test(name);
17
+ }
18
+ /** Badge colour: dev green, production red, anything else yellow. */
19
+ export function environmentColor(name, dev) {
20
+ if (isProductionEnvironment(name, dev))
21
+ return 'red';
22
+ return name === baseEnvironment(dev) ? 'green' : 'yellow';
23
+ }
24
+ /**
25
+ * The effective dev properties for one environment: the base with that
26
+ * environment's overrides applied and credentials resolved for its
27
+ * domain/username. The base ("dev") is returned as-is.
28
+ */
29
+ export function resolveEnvironment(dev, name) {
30
+ const production = isProductionEnvironment(name, dev);
31
+ if (name === baseEnvironment(dev)) {
32
+ return { ...dev, environmentName: name, productionEnvironment: production };
33
+ }
34
+ const override = dev.environments?.[name] ?? {};
35
+ const { production: _production, ...fields } = override;
36
+ const resolved = {
37
+ ...dev,
38
+ ...fields,
39
+ password: undefined,
40
+ accessToken: undefined,
41
+ sessionCookie: undefined,
42
+ environmentName: name,
43
+ productionEnvironment: production,
44
+ };
45
+ return resolveRuntimeSecrets(resolved);
46
+ }
47
+ /** A project view whose dev properties are resolved for `name`. */
48
+ export function environmentProject(project, name) {
49
+ if (!project.devProperties)
50
+ return project;
51
+ return {
52
+ ...project,
53
+ devProperties: resolveEnvironment(project.devProperties, name),
54
+ };
55
+ }
56
+ /**
57
+ * Write one environment's override into the base: keep only the keys whose
58
+ * value differs from the base, so the file stays minimal.
59
+ */
60
+ export function withEnvironmentOverride(base, name, values) {
61
+ const current = { ...base.environments?.[name] };
62
+ const changed = Object.fromEntries(Object.entries(values).map(([key, value]) => {
63
+ const baseValue = base[key];
64
+ const same = value === undefined ||
65
+ value === '' ||
66
+ JSON.stringify(value) === JSON.stringify(baseValue);
67
+ return [key, same ? undefined : value];
68
+ }));
69
+ const override = Object.fromEntries(Object.entries({ ...current, ...changed }).filter(([, value]) => value !== undefined));
70
+ return {
71
+ ...base,
72
+ environments: { ...base.environments, [name]: override },
73
+ };
74
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * UI language. English strings are the keys; `t()` returns them untouched in
3
+ * English and looks them up in the Swedish table otherwise, falling back to
4
+ * English for anything untranslated. `{name}` placeholders are filled from
5
+ * the vars argument.
6
+ */
7
+ export type Language = 'en' | 'sv';
8
+ export declare const LANGUAGES: Language[];
9
+ export declare const LANGUAGE_NAMES: Record<Language, string>;
10
+ export declare function getLanguage(): Language;
11
+ export declare function setLanguage(language: Language): void;
12
+ export declare function t(text: string, vars?: Record<string, string | number>): string;