pi-harness-runtime 1.1.391 → 1.1.393
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/AGENTS.md +29 -8
- package/harness/telegram-auth.ts +32 -0
- package/harness/telegram-update.ts +125 -0
- package/index.ts +100 -27
- package/package.json +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/dist/workspace-dotenv.d.ts +44 -0
- package/packages/contracts/dist/workspace-dotenv.js +111 -0
- package/packages/contracts/dist/workspace-env-format.d.ts +129 -6
- package/packages/contracts/dist/workspace-env-format.js +283 -11
- package/packages/contracts/dist/workspace-env.d.ts +10 -6
- package/packages/contracts/dist/workspace-env.js +36 -92
- package/packages/contracts/package.json +1 -1
- package/packages/contracts/src/workspace-dotenv.ts +114 -0
- package/packages/contracts/src/workspace-env-format.ts +318 -13
- package/packages/contracts/src/workspace-env.ts +62 -92
- package/packages/contracts/test/workspace-env.test.ts +398 -5
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/dist/adapters/telegram-adapter.d.ts +95 -0
- package/packages/notification/dist/adapters/telegram-adapter.d.ts.map +1 -1
- package/packages/notification/dist/adapters/telegram-adapter.js +284 -18
- package/packages/notification/dist/adapters/telegram-adapter.js.map +1 -1
- package/packages/notification/dist/telegram-auth.d.ts +23 -0
- package/packages/notification/dist/telegram-auth.d.ts.map +1 -0
- package/packages/notification/dist/telegram-auth.js +32 -0
- package/packages/notification/dist/telegram-auth.js.map +1 -0
- package/packages/notification/dist/types.d.ts +5 -0
- package/packages/notification/dist/types.d.ts.map +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/notification/src/adapters/telegram-adapter.ts +283 -18
- package/packages/notification/src/telegram-auth.ts +34 -0
- package/packages/notification/src/types.ts +5 -0
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/dist/server.js +7 -2
- package/packages/relay/dist/types.d.ts +3 -1
- package/packages/relay/dist/ws-server.d.ts +2 -9
- package/packages/relay/dist/ws-server.js +5 -3
- package/packages/relay/package.json +1 -1
- package/packages/relay/src/server.ts +7 -2
- package/packages/relay/src/types.ts +6 -1
- package/packages/relay/src/ws-server.ts +9 -13
- package/packages/relay/test/relay-e2e.test.ts +36 -0
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/unified-webhook-server.ts +141 -14
- package/scripts/ws-server.ts +7 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the dotenv files of a workspace.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the file system part of the environment detection: it
|
|
5
|
+
* finds the ".env" files of a workspace, reads them and skips what must not
|
|
6
|
+
* be read (templates, huge files, unreadable files).
|
|
7
|
+
*
|
|
8
|
+
* It is deliberately free of any other dependency (no node:sqlite) so it
|
|
9
|
+
* can be unit tested everywhere: the detection module itself needs
|
|
10
|
+
* node:sqlite, which not every runtime provides.
|
|
11
|
+
*/
|
|
12
|
+
import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
13
|
+
import { basename, join, relative, resolve, sep } from "node:path";
|
|
14
|
+
import { orderDotenvFileNames } from "./workspace-env-format.js";
|
|
15
|
+
/**
|
|
16
|
+
* The biggest dotenv file that is read. A ".env" file is a few kilobytes;
|
|
17
|
+
* anything bigger is not a configuration file (and reading it on every
|
|
18
|
+
* session start would be a waste).
|
|
19
|
+
*/
|
|
20
|
+
export const DOTENV_MAX_BYTES = 256 * 1024;
|
|
21
|
+
/**
|
|
22
|
+
* True for a template file that only documents the variables
|
|
23
|
+
* (".env.example", ".env.sample", ".env.template", ...).
|
|
24
|
+
*
|
|
25
|
+
* A template is not the configuration of the workspace: it must never
|
|
26
|
+
* decide the mode. The marker is looked for in EVERY segment of the
|
|
27
|
+
* suffix, so ".env.example.local" is a template too.
|
|
28
|
+
*/
|
|
29
|
+
export function isTemplateDotenvName(name) {
|
|
30
|
+
if (name === ".env")
|
|
31
|
+
return false;
|
|
32
|
+
if (!name.startsWith(".env."))
|
|
33
|
+
return false;
|
|
34
|
+
const suffix = name.slice(".env.".length);
|
|
35
|
+
return suffix
|
|
36
|
+
.split(".")
|
|
37
|
+
.some((segment) => /^(examples?|samples?|templates?|tpls?|dists?|defaults?)$/i.test(segment));
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* True when the file stays inside the workspace.
|
|
41
|
+
*
|
|
42
|
+
* A symlink may point anywhere: a ".env" that links to /etc/... is not the
|
|
43
|
+
* configuration of the workspace and must not be read.
|
|
44
|
+
*/
|
|
45
|
+
function isInsideWorkspace(workspacePath, filePath) {
|
|
46
|
+
try {
|
|
47
|
+
const root = realpathSync(resolve(workspacePath));
|
|
48
|
+
const target = realpathSync(resolve(filePath));
|
|
49
|
+
const rel = relative(root, target);
|
|
50
|
+
if (rel === "")
|
|
51
|
+
return true;
|
|
52
|
+
return !rel.startsWith("..") && !rel.startsWith(sep);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// An unreadable path cannot be inside the workspace.
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The dotenv files of a workspace as FULL paths, in the order they are read.
|
|
61
|
+
*
|
|
62
|
+
* ".env" comes first, then the well known variants, then every other
|
|
63
|
+
* ".env.*" file. Template files are skipped.
|
|
64
|
+
*/
|
|
65
|
+
export function listDotenvFiles(workspacePath) {
|
|
66
|
+
let entries = [];
|
|
67
|
+
try {
|
|
68
|
+
entries = readdirSync(workspacePath);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
entries = [];
|
|
72
|
+
}
|
|
73
|
+
const names = entries.filter((name) => (name === ".env" || name.startsWith(".env.")) && !isTemplateDotenvName(name));
|
|
74
|
+
return orderDotenvFileNames(names)
|
|
75
|
+
.filter((name) => {
|
|
76
|
+
try {
|
|
77
|
+
const path = join(workspacePath, name);
|
|
78
|
+
// isFile() is false for a directory named ".env".
|
|
79
|
+
if (!statSync(path).isFile())
|
|
80
|
+
return false;
|
|
81
|
+
// A symlink out of the workspace is not a workspace file.
|
|
82
|
+
return isInsideWorkspace(workspacePath, path);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
.map((name) => join(workspacePath, name));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Read the dotenv files of a workspace.
|
|
92
|
+
*
|
|
93
|
+
* The files are read through the full paths of listDotenvFiles, so the
|
|
94
|
+
* workspace - not the current working directory - decides which files are
|
|
95
|
+
* read.
|
|
96
|
+
*/
|
|
97
|
+
export function readDotenvFiles(workspacePath) {
|
|
98
|
+
const files = [];
|
|
99
|
+
for (const path of listDotenvFiles(workspacePath)) {
|
|
100
|
+
try {
|
|
101
|
+
const stats = statSync(path);
|
|
102
|
+
if (stats.size > DOTENV_MAX_BYTES)
|
|
103
|
+
continue;
|
|
104
|
+
files.push({ name: basename(path), content: readFileSync(path, "utf8") });
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// Ignore unreadable files.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return files;
|
|
111
|
+
}
|
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* node:sqlite dependency (bun 1.3.x has no node:sqlite module).
|
|
6
6
|
*
|
|
7
7
|
* Wording rules:
|
|
8
|
-
* - The mode is only reported when it is EXPLICIT
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* - The mode is only reported when it is EXPLICIT: the NODE_ENV of the
|
|
9
|
+
* process env, or a variable of the dotenv files of the workspace -
|
|
10
|
+
* NODE_ENV, an environment shaped key whose value names a known mode
|
|
11
|
+
* (e.g. AMOS_ENV=development) or any other environment shaped key. An
|
|
12
|
+
* unknown mode is always shown with its reason, never as a bare word next
|
|
13
|
+
* to a dev server state that could be read as proof of the mode.
|
|
11
14
|
* - A dev/build script is a CAPABILITY, not a state. The status line says
|
|
12
15
|
* "not running (script available)" so it cannot be mistaken for a running
|
|
13
16
|
* server or for evidence about the workspace mode.
|
|
@@ -24,6 +27,17 @@ export interface EnvEvidence {
|
|
|
24
27
|
key: string;
|
|
25
28
|
value: string;
|
|
26
29
|
timestamp: number;
|
|
30
|
+
/** The dotenv file a value came from (".env", ".env.local", ...). */
|
|
31
|
+
file?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* A variable found in a dotenv file.
|
|
35
|
+
*/
|
|
36
|
+
export interface DotenvVar {
|
|
37
|
+
key: string;
|
|
38
|
+
value: string;
|
|
39
|
+
/** The file the variable came from ("" for the process env). */
|
|
40
|
+
file?: string;
|
|
27
41
|
}
|
|
28
42
|
/**
|
|
29
43
|
* Workspace environment result
|
|
@@ -95,13 +109,122 @@ export declare function filterOwnPids(pids: string[], ownPids: string[]): string
|
|
|
95
109
|
* "/home/frappe/app", but not of "/home/frappeX".
|
|
96
110
|
*/
|
|
97
111
|
export declare function formatWorkspacePath(workspacePath: string, home?: string): string;
|
|
112
|
+
/**
|
|
113
|
+
* Map a raw value to a workspace mode.
|
|
114
|
+
*
|
|
115
|
+
* Returns null for a value that names no known mode ("staging", ""): the
|
|
116
|
+
* caller then reports the mode as unknown and names the value.
|
|
117
|
+
*/
|
|
118
|
+
export declare function modeFromValue(value: string): WorkspaceMode | null;
|
|
119
|
+
/**
|
|
120
|
+
* Parse a dotenv file into its variables.
|
|
121
|
+
*
|
|
122
|
+
* Only the name and the value are kept. A dotenv file holds secrets, so the
|
|
123
|
+
* detector must not copy more than it needs.
|
|
124
|
+
*/
|
|
125
|
+
export declare function parseDotenvVars(content: string): DotenvVar[];
|
|
126
|
+
/**
|
|
127
|
+
* The placeholder for a value that must not be shown.
|
|
128
|
+
*/
|
|
129
|
+
export declare const MASKED_MODE_VALUE = "<set>";
|
|
130
|
+
/**
|
|
131
|
+
* True for a key that names the environment.
|
|
132
|
+
*
|
|
133
|
+
* "NODE_ENV", "AMOS_ENV", "RAILS_ENV", "APP_ENVIRONMENT", "APP_MODE",
|
|
134
|
+
* "APP_STAGE" - but not "BRANCH" or "DEBUG": a key must look like it holds
|
|
135
|
+
* the name of an environment before its value may decide the mode.
|
|
136
|
+
*/
|
|
137
|
+
export declare function isEnvShapedKey(key: string): boolean;
|
|
138
|
+
/**
|
|
139
|
+
* True for a key that may hold a secret.
|
|
140
|
+
*/
|
|
141
|
+
export declare function isSecretLikeKey(key: string): boolean;
|
|
142
|
+
/**
|
|
143
|
+
* True when the value of the variable may be shown.
|
|
144
|
+
*
|
|
145
|
+
* The value ends up in the agent context and in the SQLite cache, so it is
|
|
146
|
+
* only shown when it is provably not a secret: the NODE_ENV of the process
|
|
147
|
+
* env, or a value that IS the name of the mode.
|
|
148
|
+
*/
|
|
149
|
+
export declare function canShowModeValue(key: string, value: string, source: "process.env" | ".env file"): boolean;
|
|
150
|
+
/**
|
|
151
|
+
* Find the variable that names the environment.
|
|
152
|
+
*
|
|
153
|
+
* The search order is the priority the user described:
|
|
154
|
+
* 1. NODE_ENV
|
|
155
|
+
* 2. an environment shaped key whose value names a known mode
|
|
156
|
+
* (development, production, test) - e.g. AMOS_ENV=development
|
|
157
|
+
* 3. any other environment shaped key (e.g. AMOS_ENV, RAILS_ENV) - it may
|
|
158
|
+
* be empty or name a mode this detector does not know
|
|
159
|
+
*
|
|
160
|
+
* A usable key wins over an unusable one: an earlier "APP_MODE=staging" or
|
|
161
|
+
* an empty "APP_ENV=" of the same file must not shadow the
|
|
162
|
+
* "AMOS_ENV=development" of that file. The value match is switched off for
|
|
163
|
+
* the process env (an unrelated variable that happens to hold the word
|
|
164
|
+
* "development" says nothing about the environment) and it is restricted to
|
|
165
|
+
* environment shaped keys: "BRANCH=production" does not make a production
|
|
166
|
+
* workspace.
|
|
167
|
+
*
|
|
168
|
+
* The value match is OPT-IN: a caller has to ask for it
|
|
169
|
+
* (`allowValueMatch: true`).
|
|
170
|
+
*/
|
|
171
|
+
export declare function detectModeHint(vars: DotenvVar[], options?: {
|
|
172
|
+
allowValueMatch?: boolean;
|
|
173
|
+
}): DotenvVar | null;
|
|
174
|
+
/**
|
|
175
|
+
* Order the dotenv file names of a workspace.
|
|
176
|
+
*
|
|
177
|
+
* The base ".env" wins over the variants, the well known variants come in a
|
|
178
|
+
* fixed order and every other ".env.*" file follows alphabetically.
|
|
179
|
+
*/
|
|
180
|
+
export declare function orderDotenvFileNames(names: string[]): string[];
|
|
181
|
+
/**
|
|
182
|
+
* Read the variables of the dotenv files of a workspace.
|
|
183
|
+
*
|
|
184
|
+
* The first file that defines a key wins (see orderDotenvFileNames).
|
|
185
|
+
*/
|
|
186
|
+
export declare function collectDotenvVars(files: {
|
|
187
|
+
name: string;
|
|
188
|
+
content: string;
|
|
189
|
+
}[]): DotenvVar[];
|
|
190
|
+
/**
|
|
191
|
+
* The variables of the process env that can name the environment.
|
|
192
|
+
*
|
|
193
|
+
* Only NODE_ENV and the environment shaped keys are collected: an unrelated
|
|
194
|
+
* variable of the process env must not outrank the workspace files.
|
|
195
|
+
*/
|
|
196
|
+
export declare function collectProcessEnvVars(env?: NodeJS.ProcessEnv): DotenvVar[];
|
|
197
|
+
/**
|
|
198
|
+
* The result of the mode detection: the mode plus the variable it came from.
|
|
199
|
+
*/
|
|
200
|
+
export interface ModeResolution {
|
|
201
|
+
mode: WorkspaceMode;
|
|
202
|
+
hint: DotenvVar | null;
|
|
203
|
+
source: "process.env" | ".env file" | null;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Resolve the workspace mode from the variables that were found.
|
|
207
|
+
*
|
|
208
|
+
* The order is:
|
|
209
|
+
* 1. the NODE_ENV of the process env - it decides, also when it names no
|
|
210
|
+
* known mode (then the mode stays unknown and the label names it: an
|
|
211
|
+
* explicit "staging" must not be overwritten by a file)
|
|
212
|
+
* 2. the dotenv files of the workspace: NODE_ENV, then an environment
|
|
213
|
+
* shaped key WHOSE VALUE names a known mode (e.g. AMOS_ENV=development),
|
|
214
|
+
* then any other environment shaped key (so that the label can name it)
|
|
215
|
+
* 3. an environment shaped key of the process env (the files are silent)
|
|
216
|
+
*/
|
|
217
|
+
export declare function resolveWorkspaceMode(processVars: DotenvVar[], dotenvVars: DotenvVar[]): ModeResolution;
|
|
98
218
|
/**
|
|
99
219
|
* Human readable mode label.
|
|
100
220
|
*
|
|
101
221
|
* "unknown" always carries the reason: the mode can only be proven by an
|
|
102
|
-
* explicit NODE_ENV
|
|
103
|
-
*
|
|
104
|
-
*
|
|
222
|
+
* explicit variable - NODE_ENV, another *_ENV key (e.g. AMOS_ENV) or a
|
|
223
|
+
* value that names a known mode. When such a variable IS present but names
|
|
224
|
+
* a mode this detector does not know ("staging"), the label says exactly
|
|
225
|
+
* that instead of claiming there is no variable at all.
|
|
226
|
+
*
|
|
227
|
+
* A known mode names its source: "DEVELOPMENT (from AMOS_ENV in .env)".
|
|
105
228
|
*/
|
|
106
229
|
export declare function describeWorkspaceMode(env: WorkspaceEnvResult): string;
|
|
107
230
|
/**
|
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
* node:sqlite dependency (bun 1.3.x has no node:sqlite module).
|
|
6
6
|
*
|
|
7
7
|
* Wording rules:
|
|
8
|
-
* - The mode is only reported when it is EXPLICIT
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* - The mode is only reported when it is EXPLICIT: the NODE_ENV of the
|
|
9
|
+
* process env, or a variable of the dotenv files of the workspace -
|
|
10
|
+
* NODE_ENV, an environment shaped key whose value names a known mode
|
|
11
|
+
* (e.g. AMOS_ENV=development) or any other environment shaped key. An
|
|
12
|
+
* unknown mode is always shown with its reason, never as a bare word next
|
|
13
|
+
* to a dev server state that could be read as proof of the mode.
|
|
11
14
|
* - A dev/build script is a CAPABILITY, not a state. The status line says
|
|
12
15
|
* "not running (script available)" so it cannot be mistaken for a running
|
|
13
16
|
* server or for evidence about the workspace mode.
|
|
@@ -94,23 +97,292 @@ export function formatWorkspacePath(workspacePath, home) {
|
|
|
94
97
|
}
|
|
95
98
|
return path;
|
|
96
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Map a raw value to a workspace mode.
|
|
102
|
+
*
|
|
103
|
+
* Returns null for a value that names no known mode ("staging", ""): the
|
|
104
|
+
* caller then reports the mode as unknown and names the value.
|
|
105
|
+
*/
|
|
106
|
+
export function modeFromValue(value) {
|
|
107
|
+
const v = value.trim().toLowerCase();
|
|
108
|
+
if (v === "production" || v === "prod")
|
|
109
|
+
return "production";
|
|
110
|
+
if (v === "development" || v === "dev")
|
|
111
|
+
return "development";
|
|
112
|
+
if (v === "test" || v === "testing")
|
|
113
|
+
return "test";
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Read the value of one dotenv line.
|
|
118
|
+
*
|
|
119
|
+
* Handles: KEY=value, KEY: value, export KEY=value, quoted values and a
|
|
120
|
+
* trailing comment. A quoted value keeps everything up to the closing quote
|
|
121
|
+
* (a "#" inside the quotes is part of the value). An inline comment of an
|
|
122
|
+
* unquoted value needs the space (" #"), like npm dotenv: a "#" inside a
|
|
123
|
+
* value ("abc#123") stays part of the value.
|
|
124
|
+
*/
|
|
125
|
+
function parseDotenvValue(raw) {
|
|
126
|
+
const value = raw.trim();
|
|
127
|
+
if (value.startsWith('"') || value.startsWith("'")) {
|
|
128
|
+
const quote = value[0];
|
|
129
|
+
const end = value.indexOf(quote, 1);
|
|
130
|
+
return end === -1 ? value.slice(1) : value.slice(1, end);
|
|
131
|
+
}
|
|
132
|
+
const hash = value.indexOf(" #");
|
|
133
|
+
if (hash !== -1)
|
|
134
|
+
return value.slice(0, hash).trim();
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Parse a dotenv file into its variables.
|
|
139
|
+
*
|
|
140
|
+
* Only the name and the value are kept. A dotenv file holds secrets, so the
|
|
141
|
+
* detector must not copy more than it needs.
|
|
142
|
+
*/
|
|
143
|
+
export function parseDotenvVars(content) {
|
|
144
|
+
const vars = [];
|
|
145
|
+
for (const rawLine of content.split("\n")) {
|
|
146
|
+
const line = rawLine.trim();
|
|
147
|
+
if (!line || line.startsWith("#"))
|
|
148
|
+
continue;
|
|
149
|
+
const match = line.match(/^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*[=:]\s*(.*)$/);
|
|
150
|
+
if (!match)
|
|
151
|
+
continue;
|
|
152
|
+
vars.push({ key: match[1], value: parseDotenvValue(match[2]) });
|
|
153
|
+
}
|
|
154
|
+
return vars;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* The placeholder for a value that must not be shown.
|
|
158
|
+
*/
|
|
159
|
+
export const MASKED_MODE_VALUE = "<set>";
|
|
160
|
+
/**
|
|
161
|
+
* True for a key that names the environment.
|
|
162
|
+
*
|
|
163
|
+
* "NODE_ENV", "AMOS_ENV", "RAILS_ENV", "APP_ENVIRONMENT", "APP_MODE",
|
|
164
|
+
* "APP_STAGE" - but not "BRANCH" or "DEBUG": a key must look like it holds
|
|
165
|
+
* the name of an environment before its value may decide the mode.
|
|
166
|
+
*/
|
|
167
|
+
export function isEnvShapedKey(key) {
|
|
168
|
+
return /(^|_)(ENV|ENVIRONMENT|MODE|STAGE)$/i.test(key);
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* True for a key that may hold a secret.
|
|
172
|
+
*/
|
|
173
|
+
export function isSecretLikeKey(key) {
|
|
174
|
+
return /(SECRET|TOKEN|PASSWORD|PASSWD|PWD|KEY|CREDENTIAL|AUTH)/i.test(key);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* True when the value of the variable may be shown.
|
|
178
|
+
*
|
|
179
|
+
* The value ends up in the agent context and in the SQLite cache, so it is
|
|
180
|
+
* only shown when it is provably not a secret: the NODE_ENV of the process
|
|
181
|
+
* env, or a value that IS the name of the mode.
|
|
182
|
+
*/
|
|
183
|
+
export function canShowModeValue(key, value, source) {
|
|
184
|
+
if (isSecretLikeKey(key))
|
|
185
|
+
return false;
|
|
186
|
+
if (source === "process.env" && key === "NODE_ENV")
|
|
187
|
+
return true;
|
|
188
|
+
return modeFromValue(value) !== null;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Find the variable that names the environment.
|
|
192
|
+
*
|
|
193
|
+
* The search order is the priority the user described:
|
|
194
|
+
* 1. NODE_ENV
|
|
195
|
+
* 2. an environment shaped key whose value names a known mode
|
|
196
|
+
* (development, production, test) - e.g. AMOS_ENV=development
|
|
197
|
+
* 3. any other environment shaped key (e.g. AMOS_ENV, RAILS_ENV) - it may
|
|
198
|
+
* be empty or name a mode this detector does not know
|
|
199
|
+
*
|
|
200
|
+
* A usable key wins over an unusable one: an earlier "APP_MODE=staging" or
|
|
201
|
+
* an empty "APP_ENV=" of the same file must not shadow the
|
|
202
|
+
* "AMOS_ENV=development" of that file. The value match is switched off for
|
|
203
|
+
* the process env (an unrelated variable that happens to hold the word
|
|
204
|
+
* "development" says nothing about the environment) and it is restricted to
|
|
205
|
+
* environment shaped keys: "BRANCH=production" does not make a production
|
|
206
|
+
* workspace.
|
|
207
|
+
*
|
|
208
|
+
* The value match is OPT-IN: a caller has to ask for it
|
|
209
|
+
* (`allowValueMatch: true`).
|
|
210
|
+
*/
|
|
211
|
+
export function detectModeHint(vars, options = {}) {
|
|
212
|
+
const nodeEnv = vars.find((v) => v.key === "NODE_ENV");
|
|
213
|
+
if (nodeEnv)
|
|
214
|
+
return nodeEnv;
|
|
215
|
+
// The value match is opt-in: a caller that passes the process env
|
|
216
|
+
// variables must not let an unrelated variable that happens to hold the
|
|
217
|
+
// word "development" decide the mode.
|
|
218
|
+
if (options.allowValueMatch === true) {
|
|
219
|
+
// An environment shaped key whose value names a known mode wins: an
|
|
220
|
+
// earlier "APP_MODE=staging" or an empty "APP_ENV=" of the same file
|
|
221
|
+
// must not shadow the "AMOS_ENV=development" of the same file.
|
|
222
|
+
const usable = vars.find((v) => v.key !== "NODE_ENV" &&
|
|
223
|
+
isEnvShapedKey(v.key) &&
|
|
224
|
+
modeFromValue(v.value) !== null);
|
|
225
|
+
if (usable)
|
|
226
|
+
return usable;
|
|
227
|
+
}
|
|
228
|
+
// No usable key: fall back to the first environment shaped key so that
|
|
229
|
+
// the label can name it (it names no known mode).
|
|
230
|
+
return vars.find((v) => v.key !== "NODE_ENV" && isEnvShapedKey(v.key)) ?? null;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* The dotenv files of a workspace, in the order they are read.
|
|
234
|
+
*
|
|
235
|
+
* ".env" comes first, then the well known variants, then every other
|
|
236
|
+
* ".env.*" file. The first file that defines A KEY wins: the base ".env" is
|
|
237
|
+
* the checked in default, so a key that is only defined in a variant is
|
|
238
|
+
* read from the variant (the precedence is per key, not per mode).
|
|
239
|
+
*/
|
|
240
|
+
const DOTENV_FILE_ORDER = [
|
|
241
|
+
".env",
|
|
242
|
+
".env.local",
|
|
243
|
+
".env.development",
|
|
244
|
+
".env.dev",
|
|
245
|
+
".env.production",
|
|
246
|
+
".env.test",
|
|
247
|
+
];
|
|
248
|
+
/**
|
|
249
|
+
* Order the dotenv file names of a workspace.
|
|
250
|
+
*
|
|
251
|
+
* The base ".env" wins over the variants, the well known variants come in a
|
|
252
|
+
* fixed order and every other ".env.*" file follows alphabetically.
|
|
253
|
+
*/
|
|
254
|
+
export function orderDotenvFileNames(names) {
|
|
255
|
+
const known = DOTENV_FILE_ORDER.filter((name) => names.includes(name));
|
|
256
|
+
const rest = names
|
|
257
|
+
.filter((name) => name.startsWith(".env.") && !DOTENV_FILE_ORDER.includes(name))
|
|
258
|
+
.sort();
|
|
259
|
+
return [...known, ...rest];
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Read the variables of the dotenv files of a workspace.
|
|
263
|
+
*
|
|
264
|
+
* The first file that defines a key wins (see orderDotenvFileNames).
|
|
265
|
+
*/
|
|
266
|
+
export function collectDotenvVars(files) {
|
|
267
|
+
const vars = [];
|
|
268
|
+
const seen = new Set();
|
|
269
|
+
for (const file of orderDotenvFileNames(files.map((f) => f.name))) {
|
|
270
|
+
const content = files.find((f) => f.name === file)?.content ?? "";
|
|
271
|
+
for (const variable of parseDotenvVars(content)) {
|
|
272
|
+
if (seen.has(variable.key))
|
|
273
|
+
continue;
|
|
274
|
+
seen.add(variable.key);
|
|
275
|
+
vars.push({ key: variable.key, value: variable.value, file });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return vars;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The variables of the process env that can name the environment.
|
|
282
|
+
*
|
|
283
|
+
* Only NODE_ENV and the environment shaped keys are collected: an unrelated
|
|
284
|
+
* variable of the process env must not outrank the workspace files.
|
|
285
|
+
*/
|
|
286
|
+
export function collectProcessEnvVars(env = process.env) {
|
|
287
|
+
const vars = [];
|
|
288
|
+
for (const key of Object.keys(env)) {
|
|
289
|
+
const value = env[key];
|
|
290
|
+
if (value === undefined)
|
|
291
|
+
continue;
|
|
292
|
+
if (key === "NODE_ENV" || isEnvShapedKey(key)) {
|
|
293
|
+
vars.push({ key, value });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return vars;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Resolve the workspace mode from the variables that were found.
|
|
300
|
+
*
|
|
301
|
+
* The order is:
|
|
302
|
+
* 1. the NODE_ENV of the process env - it decides, also when it names no
|
|
303
|
+
* known mode (then the mode stays unknown and the label names it: an
|
|
304
|
+
* explicit "staging" must not be overwritten by a file)
|
|
305
|
+
* 2. the dotenv files of the workspace: NODE_ENV, then an environment
|
|
306
|
+
* shaped key WHOSE VALUE names a known mode (e.g. AMOS_ENV=development),
|
|
307
|
+
* then any other environment shaped key (so that the label can name it)
|
|
308
|
+
* 3. an environment shaped key of the process env (the files are silent)
|
|
309
|
+
*/
|
|
310
|
+
export function resolveWorkspaceMode(processVars, dotenvVars) {
|
|
311
|
+
const processNodeEnv = processVars.find((v) => v.key === "NODE_ENV");
|
|
312
|
+
if (processNodeEnv) {
|
|
313
|
+
const mode = modeFromValue(processNodeEnv.value);
|
|
314
|
+
if (mode)
|
|
315
|
+
return { mode, hint: processNodeEnv, source: "process.env" };
|
|
316
|
+
// An explicit NODE_ENV that names no known mode decides: the mode
|
|
317
|
+
// stays unknown instead of falling through to the files.
|
|
318
|
+
return { mode: "unknown", hint: processNodeEnv, source: "process.env" };
|
|
319
|
+
}
|
|
320
|
+
const dotenvHint = detectModeHint(dotenvVars, { allowValueMatch: true });
|
|
321
|
+
if (dotenvHint) {
|
|
322
|
+
const mode = modeFromValue(dotenvHint.value);
|
|
323
|
+
if (mode)
|
|
324
|
+
return { mode, hint: dotenvHint, source: ".env file" };
|
|
325
|
+
// The workspace file is explicit - even when its value names no known
|
|
326
|
+
// mode ("APP_MODE=staging"). It must not be replaced by an unrelated
|
|
327
|
+
// variable of the process env.
|
|
328
|
+
return { mode: "unknown", hint: dotenvHint, source: ".env file" };
|
|
329
|
+
}
|
|
330
|
+
// Last resort: an environment shaped key of the process env (the files
|
|
331
|
+
// are silent).
|
|
332
|
+
const processEnvHint = processVars.find((v) => v.key !== "NODE_ENV" && isEnvShapedKey(v.key));
|
|
333
|
+
if (processEnvHint) {
|
|
334
|
+
const mode = modeFromValue(processEnvHint.value);
|
|
335
|
+
if (mode)
|
|
336
|
+
return { mode, hint: processEnvHint, source: "process.env" };
|
|
337
|
+
return { mode: "unknown", hint: processEnvHint, source: "process.env" };
|
|
338
|
+
}
|
|
339
|
+
return { mode: "unknown", hint: null, source: null };
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* The evidence entry that names the environment (if any).
|
|
343
|
+
*/
|
|
344
|
+
function findModeEvidence(env) {
|
|
345
|
+
const candidates = env.evidence.filter((e) => e.source === "process.env" || e.source === ".env file");
|
|
346
|
+
// A value that names a known mode wins over a value that cannot be read.
|
|
347
|
+
return candidates.find((e) => modeFromValue(e.value) !== null) ?? candidates[0];
|
|
348
|
+
}
|
|
97
349
|
/**
|
|
98
350
|
* Human readable mode label.
|
|
99
351
|
*
|
|
100
352
|
* "unknown" always carries the reason: the mode can only be proven by an
|
|
101
|
-
* explicit NODE_ENV
|
|
102
|
-
*
|
|
103
|
-
*
|
|
353
|
+
* explicit variable - NODE_ENV, another *_ENV key (e.g. AMOS_ENV) or a
|
|
354
|
+
* value that names a known mode. When such a variable IS present but names
|
|
355
|
+
* a mode this detector does not know ("staging"), the label says exactly
|
|
356
|
+
* that instead of claiming there is no variable at all.
|
|
357
|
+
*
|
|
358
|
+
* A known mode names its source: "DEVELOPMENT (from AMOS_ENV in .env)".
|
|
104
359
|
*/
|
|
105
360
|
export function describeWorkspaceMode(env) {
|
|
361
|
+
const hint = findModeEvidence(env);
|
|
362
|
+
const where = hint?.file ? ` in ${hint.file}` : "";
|
|
106
363
|
if (env.mode !== "unknown") {
|
|
107
|
-
return
|
|
364
|
+
return hint
|
|
365
|
+
? `${env.mode.toUpperCase()} (from ${hint.key}${where})`
|
|
366
|
+
: env.mode.toUpperCase();
|
|
108
367
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
368
|
+
if (hint) {
|
|
369
|
+
// The stored mode and the evidence can disagree (a stale cache row):
|
|
370
|
+
// when the value names a mode, that mode is shown - the label must
|
|
371
|
+
// never claim that a known mode is unknown.
|
|
372
|
+
const named = modeFromValue(hint.value);
|
|
373
|
+
if (named)
|
|
374
|
+
return `${named.toUpperCase()} (from ${hint.key}${where})`;
|
|
375
|
+
if (!hint.value)
|
|
376
|
+
return `UNKNOWN (${hint.key} is empty${where})`;
|
|
377
|
+
// A value that was masked (a secret or a dotenv value that names no
|
|
378
|
+
// known mode) is not shown - only the key and the file are.
|
|
379
|
+
if (hint.value === MASKED_MODE_VALUE) {
|
|
380
|
+
const subject = where ? `${hint.key}${where}` : `${hint.key} is set and`;
|
|
381
|
+
return `UNKNOWN (${subject} names no known mode)`;
|
|
382
|
+
}
|
|
383
|
+
return `UNKNOWN (${hint.key}=${hint.value} is not a known mode${where})`;
|
|
112
384
|
}
|
|
113
|
-
return "UNKNOWN (no
|
|
385
|
+
return "UNKNOWN (no NODE_ENV or environment variable found)";
|
|
114
386
|
}
|
|
115
387
|
export function describeServerState(stateOrRunning, hasScript) {
|
|
116
388
|
// The legacy signature (a boolean plus the script flag) is kept so that
|
|
@@ -2,22 +2,26 @@
|
|
|
2
2
|
* Workspace Environment Detector
|
|
3
3
|
*
|
|
4
4
|
* Detects if the workspace is a development environment by checking:
|
|
5
|
-
* 1. process
|
|
6
|
-
* 2. .env
|
|
5
|
+
* 1. the NODE_ENV of the process env (for build processes)
|
|
6
|
+
* 2. the dotenv files of the workspace (".env", ".env.*"): NODE_ENV, then
|
|
7
|
+
* an environment shaped key whose VALUE names a known mode (e.g.
|
|
8
|
+
* AMOS_ENV=development), then any other environment shaped key
|
|
7
9
|
* 3. package.json "dev" script presence
|
|
8
10
|
*
|
|
9
11
|
* Stores detected environment in SQLite for agent reference.
|
|
10
12
|
* Uses synchronous directory creation and exception-safe operations.
|
|
11
13
|
*/
|
|
12
|
-
import { describeCwdCommand, describeOs, describeServerState, describeWorkspaceMode, formatEnvForAgentResult, formatWorkspacePath } from "./workspace-env-format.js";
|
|
13
|
-
import type { EnvEvidence, ServerState, WorkspaceEnvResult, WorkspaceMode } from "./workspace-env-format.js";
|
|
14
|
+
import { collectDotenvVars, collectProcessEnvVars, describeCwdCommand, describeOs, describeServerState, describeWorkspaceMode, detectModeHint, formatEnvForAgentResult, formatWorkspacePath, modeFromValue, orderDotenvFileNames, parseDotenvVars, resolveWorkspaceMode } from "./workspace-env-format.js";
|
|
15
|
+
import type { DotenvVar, EnvEvidence, ServerState, WorkspaceEnvResult, WorkspaceMode } from "./workspace-env-format.js";
|
|
14
16
|
/**
|
|
15
17
|
* The mode/evidence types and every human readable rendering of the workspace
|
|
16
18
|
* environment live in workspace-env-format.ts. They are re-exported here so
|
|
17
19
|
* the existing import paths keep working.
|
|
18
20
|
*/
|
|
19
|
-
export { describeCwdCommand, describeOs, describeServerState, describeWorkspaceMode, formatEnvForAgentResult, formatWorkspacePath, };
|
|
20
|
-
export
|
|
21
|
+
export { collectDotenvVars, collectProcessEnvVars, describeCwdCommand, describeOs, describeServerState, describeWorkspaceMode, detectModeHint, formatEnvForAgentResult, formatWorkspacePath, modeFromValue, orderDotenvFileNames, parseDotenvVars, resolveWorkspaceMode, };
|
|
22
|
+
export { canShowModeValue, isEnvShapedKey, isSecretLikeKey, MASKED_MODE_VALUE } from "./workspace-env-format.js";
|
|
23
|
+
export { isTemplateDotenvName, listDotenvFiles, readDotenvFiles } from "./workspace-dotenv.js";
|
|
24
|
+
export type { DotenvVar, EnvEvidence, ServerState, WorkspaceEnvResult, WorkspaceMode };
|
|
21
25
|
/**
|
|
22
26
|
* Detect and store workspace environment
|
|
23
27
|
*/
|