@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 +4 -1
- package/dist/cli.js +27 -7
- package/dist/index.d.ts +7 -0
- package/dist/index.js +1 -1
- package/dist/{run-QGYeawST.js → run-DuN8Wqw9.js} +4 -2
- package/package.json +1 -1
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-
|
|
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
|
|
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
|
|
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-
|
|
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.
|
|
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": {
|