@philkrpp/phase-loader 0.1.2 → 0.1.5

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 CHANGED
@@ -32,7 +32,8 @@ secrets: host, app ids, paths and keyring account names only.
32
32
  }
33
33
  },
34
34
  "require": ["DATABASE_URL"],
35
- "allowLiteral": []
35
+ "allowLiteral": [],
36
+ "external": ["WORKER_TIER"]
36
37
  }
37
38
  ```
38
39
 
@@ -42,9 +43,11 @@ secrets: host, app ids, paths and keyring account names only.
42
43
  | `targets.<name>.env` | Phase environment name, e.g. `Development`. |
43
44
  | `targets.<name>.account` | OS keyring account (service `phase`) holding the service token for this target. |
44
45
  | `targets.<name>.layers` | Apps (`appId` = Phase app UUID; `app` = name for messages) and paths, exported in order; later layers win. |
46
+ | `targets.<name>.require` | Optional; replaces the top-level `require` for this target (e.g. a release target without a database). |
45
47
  | `targets.<name>.personal.account` | Optional keyring account holding a personal app id or name; exported last (path `/`). |
46
48
  | `require` | Keys the merged export must contain; otherwise loading fails and lists the missing names. |
47
49
  | `allowLiteral` | Keys whose value may start with `${`. Any other value starting with `${` is an unresolved Phase reference and fails the load. |
50
+ | `external` | Names set or read outside the Phase `dev` export: Coolify-only values, CI, runtime, variables read by libraries (`OTEL_*`). `example --write` lists them in their own section; `check` never reports them. |
48
51
 
49
52
  ## Tokens
50
53
 
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as KEYRING_SERVICE, c as loadConfig, l as PhaseLoaderError, n as fetchLayers, s as writeKeyring, t as run } from "./run-QGYeawST.js";
2
+ import { a as KEYRING_SERVICE, c as loadConfig, l as PhaseLoaderError, n as fetchLayers, s as writeKeyring, t as run } from "./run-DuN8Wqw9.js";
3
3
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { execFileSync } from "node:child_process";
@@ -35,7 +35,14 @@ function exampleNames(text) {
35
35
  * Returns the text to print; values are never read into the output.
36
36
  */
37
37
  async function example(opts = {}) {
38
- const { dir, sections } = await phaseNames(opts.cwd ?? process.cwd());
38
+ const cwd = opts.cwd ?? process.cwd();
39
+ const { dir, sections: phase } = await phaseNames(cwd);
40
+ const inPhase = new Set(phase.flatMap((s) => s.names));
41
+ const external = [...new Set(loadConfig(cwd).external)].filter((n) => !inPhase.has(n)).sort();
42
+ const sections = [...phase, {
43
+ label: "external (not loaded from Phase: Coolify, CI, runtime, libraries)",
44
+ names: external
45
+ }];
39
46
  const text = sections.filter((s) => s.names.length > 0).map((s) => `# ${s.label}\n${s.names.map((n) => `${n}=`).join("\n")}\n`).join("\n");
40
47
  const file = join(dir, EXAMPLE_FILE);
41
48
  if (opts.write) {
@@ -73,7 +80,17 @@ const BUILT_IN = /* @__PURE__ */ new Set([
73
80
  "SSR",
74
81
  "BASE_URL",
75
82
  "CI",
76
- "PHASE_ENV_LOADED"
83
+ "PHASE_ENV_LOADED",
84
+ "VITEST",
85
+ "PATH",
86
+ "HOME",
87
+ "USER",
88
+ "LOGNAME",
89
+ "SHELL",
90
+ "PWD",
91
+ "TMPDIR",
92
+ "TZ",
93
+ "LANG"
77
94
  ]);
78
95
  function trackedFiles(dir) {
79
96
  try {
@@ -84,7 +101,8 @@ function trackedFiles(dir) {
84
101
  "ignore",
85
102
  "pipe",
86
103
  "ignore"
87
- ]
104
+ ],
105
+ maxBuffer: 268435456
88
106
  }).split("\0").filter(Boolean);
89
107
  } catch {
90
108
  throw new PhaseLoaderError(`"git ls-files" failed in ${dir}: check needs a git repository.`);
@@ -114,15 +132,17 @@ function usedNames(dir) {
114
132
  * Online: also against the key names in Phase (`dev`). Names only, never values. `ok` is false on any finding.
115
133
  */
116
134
  async function check(opts = {}) {
117
- const dir = loadConfig(opts.cwd ?? process.cwd()).dir;
135
+ const config = loadConfig(opts.cwd ?? process.cwd());
136
+ const dir = config.dir;
118
137
  const file = join(dir, EXAMPLE_FILE);
119
138
  if (!existsSync(file)) throw new PhaseLoaderError(`${EXAMPLE_FILE} not found in ${dir} (create it with "phase-loader example --write").`);
139
+ const external = new Set(config.external);
120
140
  const documented = new Set(exampleNames(readFileSync(file, "utf8")));
121
141
  const { used, quoted } = usedNames(dir);
122
- const findings = [[`Used in code but undocumented in ${EXAMPLE_FILE}:`, [...used].filter((n) => !documented.has(n))], [`Orphan in ${EXAMPLE_FILE} (never read in tracked code):`, [...documented].filter((n) => !used.has(n) && !quoted.has(n))]];
142
+ const findings = [[`Used in code but undocumented in ${EXAMPLE_FILE}:`, [...used].filter((n) => !documented.has(n) && !external.has(n))], [`Orphan in ${EXAMPLE_FILE} (never read in tracked code):`, [...documented].filter((n) => !used.has(n) && !quoted.has(n) && !external.has(n))]];
123
143
  if (!opts.offline) {
124
144
  const inPhase = new Set((await phaseNames(opts.cwd ?? process.cwd())).sections.flatMap((s) => s.names));
125
- findings.push([`In ${EXAMPLE_FILE} but not in Phase:`, [...documented].filter((n) => !inPhase.has(n))], [`In Phase but not in ${EXAMPLE_FILE}:`, [...inPhase].filter((n) => !documented.has(n))]);
145
+ findings.push([`In ${EXAMPLE_FILE} but not in Phase:`, [...documented].filter((n) => !inPhase.has(n) && !external.has(n))], [`In Phase but not in ${EXAMPLE_FILE}:`, [...inPhase].filter((n) => !documented.has(n))]);
126
146
  }
127
147
  const lines = findings.filter(([, names]) => names.length > 0).flatMap(([title, names]) => [title, ` ${names.sort().join(", ")}`]);
128
148
  return lines.length > 0 ? {
package/dist/index.d.ts CHANGED
@@ -16,6 +16,8 @@ interface PhaseTarget {
16
16
  personal?: {
17
17
  account: string;
18
18
  };
19
+ /** Replaces the top-level `require` for this target (e.g. a release target without a database). */
20
+ require?: string[];
19
21
  }
20
22
  interface PhaseConfig {
21
23
  /** Directory that contains `phase.config.json`. */
@@ -26,6 +28,11 @@ interface PhaseConfig {
26
28
  require: string[];
27
29
  /** Keys whose value may start with `${` (not a Phase reference). */
28
30
  allowLiteral: string[];
31
+ /**
32
+ * Names read or set outside the Phase `dev` export (Coolify-only values, CI, runtime, libraries such as OTEL_*):
33
+ * listed in their own `.env.example` section and never reported by `check`.
34
+ */
35
+ external: string[];
29
36
  }
30
37
  /** Finds the nearest `phase.config.json` from `cwd` upwards, validates and returns it. */
31
38
  export declare function loadConfig(cwd: string): PhaseConfig;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { l as PhaseLoaderError, c as loadConfig, r as loadPhaseEnv, i as loadPhaseEnvSync, o as readToken, t as run } from "./run-QGYeawST.js";
1
+ export { l as PhaseLoaderError, c as loadConfig, r as loadPhaseEnv, i as loadPhaseEnvSync, o as readToken, t as run } from "./run-DuN8Wqw9.js";
@@ -77,6 +77,7 @@ function parseConfig(file, dir) {
77
77
  if (!isObject$1(value.personal)) return fail(`${at}: "personal" must be an object`);
78
78
  target.personal = { account: str(value.personal.account, ACCOUNT, `${at}: "personal.account" must be a keyring account name`) };
79
79
  }
80
+ if (value.require !== void 0) target.require = keys(value.require, `targets.${name}.require`);
80
81
  targets[name] = target;
81
82
  }
82
83
  return {
@@ -84,7 +85,8 @@ function parseConfig(file, dir) {
84
85
  host,
85
86
  targets,
86
87
  require: keys(raw.require, "require"),
87
- allowLiteral: keys(raw.allowLiteral, "allowLiteral")
88
+ allowLiteral: keys(raw.allowLiteral, "allowLiteral"),
89
+ external: keys(raw.external, "external")
88
90
  };
89
91
  }
90
92
  function isObject$1(value) {
@@ -231,7 +233,7 @@ async function fetchLayers(target, cwd) {
231
233
  function merge(target, config, fetched, keep) {
232
234
  const exported = {};
233
235
  for (const layer of fetched) for (const [key, value] of layer.entries) exported[key] = value;
234
- const missing = config.require.filter((key) => !Object.hasOwn(exported, key));
236
+ const missing = (config.targets[target]?.require ?? config.require).filter((key) => !Object.hasOwn(exported, key));
235
237
  if (missing.length > 0) throw new PhaseLoaderError(`Phase export for "${target}" lacks required key(s): ${missing.join(", ")}. Check the token's access.`);
236
238
  const env = {};
237
239
  for (const [key, value] of Object.entries(exported)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@philkrpp/phase-loader",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "Load env values from a self-hosted Phase secrets manager into a child process or the current process, with layered apps/paths and OS keyring tokens.",
5
5
  "license": "MIT",
6
6
  "repository": {