@agent-native/core 0.7.57 → 0.7.58

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 (30) hide show
  1. package/dist/cli/create.d.ts.map +1 -1
  2. package/dist/cli/create.js +2 -0
  3. package/dist/cli/create.js.map +1 -1
  4. package/dist/client/NewWorkspaceAppFlow.d.ts.map +1 -1
  5. package/dist/client/NewWorkspaceAppFlow.js +34 -38
  6. package/dist/client/NewWorkspaceAppFlow.js.map +1 -1
  7. package/dist/deploy/workspace-deploy.d.ts.map +1 -1
  8. package/dist/deploy/workspace-deploy.js +7 -20
  9. package/dist/deploy/workspace-deploy.js.map +1 -1
  10. package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
  11. package/dist/integrations/a2a-continuation-processor.js +17 -1
  12. package/dist/integrations/a2a-continuation-processor.js.map +1 -1
  13. package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
  14. package/dist/integrations/a2a-continuations-store.js +14 -5
  15. package/dist/integrations/a2a-continuations-store.js.map +1 -1
  16. package/dist/shared/workspace-app-id.d.ts +1 -1
  17. package/dist/shared/workspace-app-id.d.ts.map +1 -1
  18. package/dist/shared/workspace-app-id.js +2 -0
  19. package/dist/shared/workspace-app-id.js.map +1 -1
  20. package/dist/templates/workspace-root/.env.example +6 -0
  21. package/dist/templates/workspace-root/AGENTS.md +50 -0
  22. package/dist/templates/workspace-root/README.md +18 -1
  23. package/dist/templates/workspace-root/package.json +2 -0
  24. package/dist/templates/workspace-root/scripts/repair-workspace-org.ts +283 -0
  25. package/package.json +1 -1
  26. package/src/templates/workspace-root/.env.example +6 -0
  27. package/src/templates/workspace-root/AGENTS.md +50 -0
  28. package/src/templates/workspace-root/README.md +18 -1
  29. package/src/templates/workspace-root/package.json +2 -0
  30. package/src/templates/workspace-root/scripts/repair-workspace-org.ts +283 -0
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ import crypto from "node:crypto";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+
7
+ interface Options {
8
+ name?: string;
9
+ domain?: string;
10
+ ownerEmail?: string;
11
+ a2aSecret?: string;
12
+ envPath: string;
13
+ force: boolean;
14
+ dryRun: boolean;
15
+ setDispatchDefaultOwner: boolean;
16
+ }
17
+
18
+ const HELP = `Usage:
19
+ pnpm repair:workspace-org -- --name "Example Co" --domain example.com --owner-email owner@example.com
20
+
21
+ Options:
22
+ --name <value> Sets WORKSPACE_ORG_NAME
23
+ --domain <value> Sets WORKSPACE_ORG_DOMAIN
24
+ --owner-email <value> Sets WORKSPACE_OWNER_EMAIL
25
+ --a2a-secret <value> Sets A2A_SECRET (generated when omitted)
26
+ --env <path> Env file to update (default: .env)
27
+ --force Overwrite existing non-empty values
28
+ --dry-run Print the changes without writing
29
+ --set-dispatch-default-owner Also set DISPATCH_DEFAULT_OWNER_EMAIL
30
+ `;
31
+
32
+ const REQUIRED_KEYS = [
33
+ "WORKSPACE_ORG_NAME",
34
+ "WORKSPACE_ORG_DOMAIN",
35
+ "WORKSPACE_OWNER_EMAIL",
36
+ "A2A_SECRET",
37
+ ] as const;
38
+
39
+ type RequiredKey = (typeof REQUIRED_KEYS)[number];
40
+
41
+ function parseArgs(argv: string[]): Options {
42
+ const opts: Options = {
43
+ envPath: ".env",
44
+ force: false,
45
+ dryRun: false,
46
+ setDispatchDefaultOwner: false,
47
+ };
48
+
49
+ for (let i = 0; i < argv.length; i++) {
50
+ const arg = argv[i];
51
+ const [flag, inline] = arg.includes("=")
52
+ ? arg.split(/=(.*)/s, 2)
53
+ : [arg, undefined];
54
+ const value = (): string => {
55
+ const next = inline ?? argv[++i];
56
+ if (!next) fail(`Missing value for ${flag}.`);
57
+ return next;
58
+ };
59
+
60
+ switch (flag) {
61
+ case "--help":
62
+ case "-h":
63
+ console.log(HELP);
64
+ process.exit(0);
65
+ case "--name":
66
+ opts.name = value();
67
+ break;
68
+ case "--domain":
69
+ opts.domain = value();
70
+ break;
71
+ case "--owner-email":
72
+ opts.ownerEmail = value();
73
+ break;
74
+ case "--a2a-secret":
75
+ opts.a2aSecret = value();
76
+ break;
77
+ case "--env":
78
+ opts.envPath = value();
79
+ break;
80
+ case "--force":
81
+ opts.force = true;
82
+ break;
83
+ case "--dry-run":
84
+ opts.dryRun = true;
85
+ break;
86
+ case "--set-dispatch-default-owner":
87
+ opts.setDispatchDefaultOwner = true;
88
+ break;
89
+ default:
90
+ fail(`Unknown option: ${arg}\n\n${HELP}`);
91
+ }
92
+ }
93
+
94
+ return opts;
95
+ }
96
+
97
+ function fail(message: string): never {
98
+ console.error(message);
99
+ process.exit(1);
100
+ }
101
+
102
+ function readEnvFile(envPath: string): string {
103
+ if (fs.existsSync(envPath)) return fs.readFileSync(envPath, "utf-8");
104
+ const examplePath = path.join(path.dirname(envPath), ".env.example");
105
+ if (fs.existsSync(examplePath)) return fs.readFileSync(examplePath, "utf-8");
106
+ return "";
107
+ }
108
+
109
+ function parseEnv(content: string): Record<string, string> {
110
+ const values: Record<string, string> = {};
111
+ for (const line of content.split(/\r?\n/)) {
112
+ const match = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
113
+ if (!match) continue;
114
+ values[match[1]] = unquote(match[2]);
115
+ }
116
+ return values;
117
+ }
118
+
119
+ function unquote(value: string): string {
120
+ const trimmed = value.trim();
121
+ if (
122
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
123
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
124
+ ) {
125
+ return trimmed.slice(1, -1);
126
+ }
127
+ return trimmed;
128
+ }
129
+
130
+ function normalizeDomain(domain: string): string {
131
+ return domain
132
+ .trim()
133
+ .toLowerCase()
134
+ .replace(/^https?:\/\//, "")
135
+ .replace(/\/.*$/, "");
136
+ }
137
+
138
+ function validateDomain(domain: string): void {
139
+ if (!/^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/.test(domain)) {
140
+ fail(`Invalid --domain "${domain}". Use a bare domain like example.com.`);
141
+ }
142
+ }
143
+
144
+ function validateEmail(email: string): void {
145
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
146
+ fail(`Invalid --owner-email "${email}".`);
147
+ }
148
+ }
149
+
150
+ function formatEnvValue(value: string): string {
151
+ if (/^[A-Za-z0-9_@./:+-]+$/.test(value)) return value;
152
+ return JSON.stringify(value);
153
+ }
154
+
155
+ function upsertEnvValue(
156
+ content: string,
157
+ key: string,
158
+ value: string,
159
+ force: boolean,
160
+ ): { content: string; changed: boolean; skipped: boolean } {
161
+ const lines = content.length > 0 ? content.split(/\r?\n/) : [];
162
+ let found = false;
163
+ let changed = false;
164
+ let skipped = false;
165
+
166
+ const next = lines.map((line) => {
167
+ const match = line.match(/^(\s*)([A-Z0-9_]+)(\s*=\s*)(.*)$/);
168
+ if (!match || match[2] !== key) return line;
169
+
170
+ found = true;
171
+ const existing = unquote(match[4]);
172
+ if (existing && !force) {
173
+ skipped = true;
174
+ return line;
175
+ }
176
+
177
+ const replacement = `${match[1]}${key}${match[3]}${formatEnvValue(value)}`;
178
+ if (replacement !== line) changed = true;
179
+ return replacement;
180
+ });
181
+
182
+ if (!found) {
183
+ if (next.length > 0 && next[next.length - 1] !== "") next.push("");
184
+ next.push(`${key}=${formatEnvValue(value)}`);
185
+ changed = true;
186
+ }
187
+
188
+ return { content: next.join("\n"), changed, skipped };
189
+ }
190
+
191
+ function firstValue(...values: Array<string | undefined>): string | undefined {
192
+ return values.map((v) => v?.trim()).find(Boolean);
193
+ }
194
+
195
+ function main(): void {
196
+ const opts = parseArgs(process.argv.slice(2));
197
+ const envPath = path.resolve(opts.envPath);
198
+ const original = readEnvFile(envPath);
199
+ const current = parseEnv(original);
200
+
201
+ const name = firstValue(
202
+ opts.name,
203
+ process.env.WORKSPACE_ORG_NAME,
204
+ current.WORKSPACE_ORG_NAME,
205
+ );
206
+ const rawDomain = firstValue(
207
+ opts.domain,
208
+ process.env.WORKSPACE_ORG_DOMAIN,
209
+ current.WORKSPACE_ORG_DOMAIN,
210
+ );
211
+ const ownerEmail = firstValue(
212
+ opts.ownerEmail,
213
+ process.env.WORKSPACE_OWNER_EMAIL,
214
+ current.WORKSPACE_OWNER_EMAIL,
215
+ )?.toLowerCase();
216
+ const a2aSecret =
217
+ firstValue(opts.a2aSecret, process.env.A2A_SECRET, current.A2A_SECRET) ??
218
+ crypto.randomBytes(32).toString("hex");
219
+
220
+ if (!name) fail("--name or WORKSPACE_ORG_NAME is required.");
221
+ if (!rawDomain) fail("--domain or WORKSPACE_ORG_DOMAIN is required.");
222
+ if (!ownerEmail) fail("--owner-email or WORKSPACE_OWNER_EMAIL is required.");
223
+
224
+ const domain = normalizeDomain(rawDomain);
225
+ validateDomain(domain);
226
+ validateEmail(ownerEmail);
227
+
228
+ const desired: Record<RequiredKey, string> = {
229
+ WORKSPACE_ORG_NAME: name,
230
+ WORKSPACE_ORG_DOMAIN: domain,
231
+ WORKSPACE_OWNER_EMAIL: ownerEmail,
232
+ A2A_SECRET: a2aSecret,
233
+ };
234
+
235
+ let next = original.trimEnd();
236
+ const changed: string[] = [];
237
+ const skipped: string[] = [];
238
+
239
+ for (const key of REQUIRED_KEYS) {
240
+ const result = upsertEnvValue(next, key, desired[key], opts.force);
241
+ next = result.content;
242
+ if (result.changed) changed.push(key);
243
+ if (result.skipped) skipped.push(key);
244
+ }
245
+
246
+ if (opts.setDispatchDefaultOwner) {
247
+ const result = upsertEnvValue(
248
+ next,
249
+ "DISPATCH_DEFAULT_OWNER_EMAIL",
250
+ ownerEmail,
251
+ opts.force,
252
+ );
253
+ next = result.content;
254
+ if (result.changed) changed.push("DISPATCH_DEFAULT_OWNER_EMAIL");
255
+ if (result.skipped) skipped.push("DISPATCH_DEFAULT_OWNER_EMAIL");
256
+ }
257
+
258
+ next = next.trimEnd() + "\n";
259
+
260
+ if (opts.dryRun) {
261
+ console.log(next);
262
+ } else {
263
+ fs.writeFileSync(envPath, next);
264
+ }
265
+
266
+ console.log(
267
+ `${opts.dryRun ? "Validated" : "Updated"} ${path.relative(process.cwd(), envPath) || envPath}.`,
268
+ );
269
+ if (changed.length > 0) console.log(`Changed: ${changed.join(", ")}`);
270
+ if (skipped.length > 0) {
271
+ console.log(
272
+ `Kept existing values: ${skipped.join(", ")} (use --force to overwrite)`,
273
+ );
274
+ }
275
+ console.log("");
276
+ console.log("Next steps:");
277
+ console.log("1. Sign in as WORKSPACE_OWNER_EMAIL.");
278
+ console.log("2. Create or select the org named by WORKSPACE_ORG_NAME.");
279
+ console.log("3. Set the org allowed domain to WORKSPACE_ORG_DOMAIN.");
280
+ console.log("4. Set or sync the org A2A secret from A2A_SECRET across apps.");
281
+ }
282
+
283
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-native/core",
3
- "version": "0.7.57",
3
+ "version": "0.7.58",
4
4
  "type": "module",
5
5
  "description": "Framework for agent-native application development — where AI agents and UI share state via files",
6
6
  "license": "MIT",
@@ -23,6 +23,12 @@ ANTHROPIC_API_KEY=
23
23
  # Optional: OpenAI key if any app uses OpenAI engines.
24
24
  OPENAI_API_KEY=
25
25
 
26
+ # Workspace organization identity. Use operator-owned values; do not use
27
+ # company-specific examples or copied production credentials.
28
+ WORKSPACE_ORG_NAME=
29
+ WORKSPACE_ORG_DOMAIN=
30
+ WORKSPACE_OWNER_EMAIL=
31
+
26
32
  # Builder browser integration (run `agent-native dev` and visit any app,
27
33
  # then click "Connect Builder" — these are written automatically by the
28
34
  # callback handler to this workspace-level .env).
@@ -0,0 +1,50 @@
1
+ # {{APP_TITLE}} Workspace Instructions
2
+
3
+ These instructions apply at the workspace root. App-specific behavior belongs
4
+ in `apps/<app>/AGENTS.md`; shared cross-app behavior belongs in
5
+ `packages/shared/AGENTS.md` or `packages/shared/.agents/skills/`.
6
+
7
+ ## Workspace Scope
8
+
9
+ - Keep root changes focused on workspace orchestration, shared configuration,
10
+ deploy settings, and monorepo tooling.
11
+ - Keep application routes, actions, server plugins, and app state inside the
12
+ relevant `apps/<app>` directory unless multiple apps need the same behavior.
13
+ - Put reusable code in `packages/shared` only after at least two apps need it.
14
+ - Never copy live credentials, personal email addresses, customer data, or
15
+ company-specific placeholder values into source files.
16
+
17
+ ## Workspace Identity
18
+
19
+ Use the workspace root `.env` for shared identity and cross-app trust settings:
20
+
21
+ - `WORKSPACE_ORG_NAME` — human-readable organization name.
22
+ - `WORKSPACE_ORG_DOMAIN` — bare domain owned by the workspace, with no protocol
23
+ or path.
24
+ - `WORKSPACE_OWNER_EMAIL` — initial owner/admin email for repairs and
25
+ integration defaults.
26
+ - `A2A_SECRET` — shared secret for cross-app A2A signing. Generate with
27
+ `openssl rand -hex 32` or `pnpm repair:workspace-org -- --name ...`.
28
+
29
+ `DISPATCH_DEFAULT_OWNER_EMAIL` is optional. Set it only for trusted,
30
+ single-workspace deployments where unlinked integration requests should run as
31
+ the workspace owner, and prefer the same value as `WORKSPACE_OWNER_EMAIL`.
32
+
33
+ ## Org Repair
34
+
35
+ When asked to repair workspace org or A2A configuration:
36
+
37
+ 1. Read `.env` first. Do not infer the organization, domain, owner email, or
38
+ secret from old examples.
39
+ 2. Run `pnpm repair:workspace-org -- --name "<org>" --domain example.com --owner-email owner@example.com`
40
+ to create or update generic workspace identity values.
41
+ 3. Prefer the app's organization settings UI or authenticated org routes for
42
+ changing `allowed_domain` and `a2a_secret`.
43
+ 4. If direct SQL is unavoidable, inspect the live schema first and use only
44
+ parameterized `INSERT` or `UPDATE` statements. Ensure the target org has
45
+ `organizations.name`, `organizations.allowed_domain`,
46
+ `organizations.a2a_secret`, and an `org_members` owner row for
47
+ `WORKSPACE_OWNER_EMAIL`.
48
+ 5. Never use `DROP`, `TRUNCATE`, destructive `ALTER`, or an unscoped
49
+ `DELETE`. Do not rotate `A2A_SECRET` without updating every app that trusts
50
+ it.
@@ -37,7 +37,8 @@ the shared package (`@{{APP_NAME}}/shared`).
37
37
 
38
38
  ```bash
39
39
  pnpm install
40
- cp .env.example .env # fill in DATABASE_URL, BETTER_AUTH_SECRET, ANTHROPIC_API_KEY
40
+ cp .env.example .env # fill in DATABASE_URL, BETTER_AUTH_SECRET, and an LLM provider key
41
+ pnpm repair:workspace-org -- --name "Example Co" --domain example.com --owner-email owner@example.com
41
42
  pnpm dev # starts the workspace gateway and opens Dispatch
42
43
  ```
43
44
 
@@ -45,6 +46,22 @@ The dev gateway serves Dispatch at `/dispatch` and every app at its own path
45
46
  such as `/starter`. It watches `apps/`, so newly-created apps are detected and
46
47
  started without restarting `pnpm dev`.
47
48
 
49
+ ## Workspace org identity
50
+
51
+ Set these root `.env` values before production deploys or when repairing
52
+ cross-app trust:
53
+
54
+ - `WORKSPACE_ORG_NAME` — the organization name users should see.
55
+ - `WORKSPACE_ORG_DOMAIN` — the bare email/domain claim used for org matching.
56
+ - `WORKSPACE_OWNER_EMAIL` — the owner/admin email to use for bootstrap or
57
+ integration fallback.
58
+ - `A2A_SECRET` — shared signing secret for cross-app A2A calls.
59
+
60
+ Run `pnpm repair:workspace-org -- --name "<org>" --domain example.com --owner-email owner@example.com`
61
+ to fill or validate those values without committing secrets. Existing
62
+ organization rows should still be repaired through the app's org settings UI or
63
+ authenticated org routes whenever possible.
64
+
48
65
  ## Adding a new app
49
66
 
50
67
  ```bash
@@ -6,6 +6,7 @@
6
6
  "dev": "agent-native dev",
7
7
  "build": "pnpm -r build",
8
8
  "typecheck": "pnpm -r typecheck",
9
+ "repair:workspace-org": "tsx scripts/repair-workspace-org.ts",
9
10
  "fmt:check": "prettier --check .",
10
11
  "lint": "pnpm fmt:check"
11
12
  },
@@ -19,6 +20,7 @@
19
20
  "devDependencies": {
20
21
  "@types/node": "^24.2.1",
21
22
  "prettier": "^3.6.2",
23
+ "tsx": "catalog:",
22
24
  "typescript": "^6.0.3"
23
25
  },
24
26
  "packageManager": "pnpm@10.14.0"
@@ -0,0 +1,283 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ import crypto from "node:crypto";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+
7
+ interface Options {
8
+ name?: string;
9
+ domain?: string;
10
+ ownerEmail?: string;
11
+ a2aSecret?: string;
12
+ envPath: string;
13
+ force: boolean;
14
+ dryRun: boolean;
15
+ setDispatchDefaultOwner: boolean;
16
+ }
17
+
18
+ const HELP = `Usage:
19
+ pnpm repair:workspace-org -- --name "Example Co" --domain example.com --owner-email owner@example.com
20
+
21
+ Options:
22
+ --name <value> Sets WORKSPACE_ORG_NAME
23
+ --domain <value> Sets WORKSPACE_ORG_DOMAIN
24
+ --owner-email <value> Sets WORKSPACE_OWNER_EMAIL
25
+ --a2a-secret <value> Sets A2A_SECRET (generated when omitted)
26
+ --env <path> Env file to update (default: .env)
27
+ --force Overwrite existing non-empty values
28
+ --dry-run Print the changes without writing
29
+ --set-dispatch-default-owner Also set DISPATCH_DEFAULT_OWNER_EMAIL
30
+ `;
31
+
32
+ const REQUIRED_KEYS = [
33
+ "WORKSPACE_ORG_NAME",
34
+ "WORKSPACE_ORG_DOMAIN",
35
+ "WORKSPACE_OWNER_EMAIL",
36
+ "A2A_SECRET",
37
+ ] as const;
38
+
39
+ type RequiredKey = (typeof REQUIRED_KEYS)[number];
40
+
41
+ function parseArgs(argv: string[]): Options {
42
+ const opts: Options = {
43
+ envPath: ".env",
44
+ force: false,
45
+ dryRun: false,
46
+ setDispatchDefaultOwner: false,
47
+ };
48
+
49
+ for (let i = 0; i < argv.length; i++) {
50
+ const arg = argv[i];
51
+ const [flag, inline] = arg.includes("=")
52
+ ? arg.split(/=(.*)/s, 2)
53
+ : [arg, undefined];
54
+ const value = (): string => {
55
+ const next = inline ?? argv[++i];
56
+ if (!next) fail(`Missing value for ${flag}.`);
57
+ return next;
58
+ };
59
+
60
+ switch (flag) {
61
+ case "--help":
62
+ case "-h":
63
+ console.log(HELP);
64
+ process.exit(0);
65
+ case "--name":
66
+ opts.name = value();
67
+ break;
68
+ case "--domain":
69
+ opts.domain = value();
70
+ break;
71
+ case "--owner-email":
72
+ opts.ownerEmail = value();
73
+ break;
74
+ case "--a2a-secret":
75
+ opts.a2aSecret = value();
76
+ break;
77
+ case "--env":
78
+ opts.envPath = value();
79
+ break;
80
+ case "--force":
81
+ opts.force = true;
82
+ break;
83
+ case "--dry-run":
84
+ opts.dryRun = true;
85
+ break;
86
+ case "--set-dispatch-default-owner":
87
+ opts.setDispatchDefaultOwner = true;
88
+ break;
89
+ default:
90
+ fail(`Unknown option: ${arg}\n\n${HELP}`);
91
+ }
92
+ }
93
+
94
+ return opts;
95
+ }
96
+
97
+ function fail(message: string): never {
98
+ console.error(message);
99
+ process.exit(1);
100
+ }
101
+
102
+ function readEnvFile(envPath: string): string {
103
+ if (fs.existsSync(envPath)) return fs.readFileSync(envPath, "utf-8");
104
+ const examplePath = path.join(path.dirname(envPath), ".env.example");
105
+ if (fs.existsSync(examplePath)) return fs.readFileSync(examplePath, "utf-8");
106
+ return "";
107
+ }
108
+
109
+ function parseEnv(content: string): Record<string, string> {
110
+ const values: Record<string, string> = {};
111
+ for (const line of content.split(/\r?\n/)) {
112
+ const match = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
113
+ if (!match) continue;
114
+ values[match[1]] = unquote(match[2]);
115
+ }
116
+ return values;
117
+ }
118
+
119
+ function unquote(value: string): string {
120
+ const trimmed = value.trim();
121
+ if (
122
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
123
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
124
+ ) {
125
+ return trimmed.slice(1, -1);
126
+ }
127
+ return trimmed;
128
+ }
129
+
130
+ function normalizeDomain(domain: string): string {
131
+ return domain
132
+ .trim()
133
+ .toLowerCase()
134
+ .replace(/^https?:\/\//, "")
135
+ .replace(/\/.*$/, "");
136
+ }
137
+
138
+ function validateDomain(domain: string): void {
139
+ if (!/^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/.test(domain)) {
140
+ fail(`Invalid --domain "${domain}". Use a bare domain like example.com.`);
141
+ }
142
+ }
143
+
144
+ function validateEmail(email: string): void {
145
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
146
+ fail(`Invalid --owner-email "${email}".`);
147
+ }
148
+ }
149
+
150
+ function formatEnvValue(value: string): string {
151
+ if (/^[A-Za-z0-9_@./:+-]+$/.test(value)) return value;
152
+ return JSON.stringify(value);
153
+ }
154
+
155
+ function upsertEnvValue(
156
+ content: string,
157
+ key: string,
158
+ value: string,
159
+ force: boolean,
160
+ ): { content: string; changed: boolean; skipped: boolean } {
161
+ const lines = content.length > 0 ? content.split(/\r?\n/) : [];
162
+ let found = false;
163
+ let changed = false;
164
+ let skipped = false;
165
+
166
+ const next = lines.map((line) => {
167
+ const match = line.match(/^(\s*)([A-Z0-9_]+)(\s*=\s*)(.*)$/);
168
+ if (!match || match[2] !== key) return line;
169
+
170
+ found = true;
171
+ const existing = unquote(match[4]);
172
+ if (existing && !force) {
173
+ skipped = true;
174
+ return line;
175
+ }
176
+
177
+ const replacement = `${match[1]}${key}${match[3]}${formatEnvValue(value)}`;
178
+ if (replacement !== line) changed = true;
179
+ return replacement;
180
+ });
181
+
182
+ if (!found) {
183
+ if (next.length > 0 && next[next.length - 1] !== "") next.push("");
184
+ next.push(`${key}=${formatEnvValue(value)}`);
185
+ changed = true;
186
+ }
187
+
188
+ return { content: next.join("\n"), changed, skipped };
189
+ }
190
+
191
+ function firstValue(...values: Array<string | undefined>): string | undefined {
192
+ return values.map((v) => v?.trim()).find(Boolean);
193
+ }
194
+
195
+ function main(): void {
196
+ const opts = parseArgs(process.argv.slice(2));
197
+ const envPath = path.resolve(opts.envPath);
198
+ const original = readEnvFile(envPath);
199
+ const current = parseEnv(original);
200
+
201
+ const name = firstValue(
202
+ opts.name,
203
+ process.env.WORKSPACE_ORG_NAME,
204
+ current.WORKSPACE_ORG_NAME,
205
+ );
206
+ const rawDomain = firstValue(
207
+ opts.domain,
208
+ process.env.WORKSPACE_ORG_DOMAIN,
209
+ current.WORKSPACE_ORG_DOMAIN,
210
+ );
211
+ const ownerEmail = firstValue(
212
+ opts.ownerEmail,
213
+ process.env.WORKSPACE_OWNER_EMAIL,
214
+ current.WORKSPACE_OWNER_EMAIL,
215
+ )?.toLowerCase();
216
+ const a2aSecret =
217
+ firstValue(opts.a2aSecret, process.env.A2A_SECRET, current.A2A_SECRET) ??
218
+ crypto.randomBytes(32).toString("hex");
219
+
220
+ if (!name) fail("--name or WORKSPACE_ORG_NAME is required.");
221
+ if (!rawDomain) fail("--domain or WORKSPACE_ORG_DOMAIN is required.");
222
+ if (!ownerEmail) fail("--owner-email or WORKSPACE_OWNER_EMAIL is required.");
223
+
224
+ const domain = normalizeDomain(rawDomain);
225
+ validateDomain(domain);
226
+ validateEmail(ownerEmail);
227
+
228
+ const desired: Record<RequiredKey, string> = {
229
+ WORKSPACE_ORG_NAME: name,
230
+ WORKSPACE_ORG_DOMAIN: domain,
231
+ WORKSPACE_OWNER_EMAIL: ownerEmail,
232
+ A2A_SECRET: a2aSecret,
233
+ };
234
+
235
+ let next = original.trimEnd();
236
+ const changed: string[] = [];
237
+ const skipped: string[] = [];
238
+
239
+ for (const key of REQUIRED_KEYS) {
240
+ const result = upsertEnvValue(next, key, desired[key], opts.force);
241
+ next = result.content;
242
+ if (result.changed) changed.push(key);
243
+ if (result.skipped) skipped.push(key);
244
+ }
245
+
246
+ if (opts.setDispatchDefaultOwner) {
247
+ const result = upsertEnvValue(
248
+ next,
249
+ "DISPATCH_DEFAULT_OWNER_EMAIL",
250
+ ownerEmail,
251
+ opts.force,
252
+ );
253
+ next = result.content;
254
+ if (result.changed) changed.push("DISPATCH_DEFAULT_OWNER_EMAIL");
255
+ if (result.skipped) skipped.push("DISPATCH_DEFAULT_OWNER_EMAIL");
256
+ }
257
+
258
+ next = next.trimEnd() + "\n";
259
+
260
+ if (opts.dryRun) {
261
+ console.log(next);
262
+ } else {
263
+ fs.writeFileSync(envPath, next);
264
+ }
265
+
266
+ console.log(
267
+ `${opts.dryRun ? "Validated" : "Updated"} ${path.relative(process.cwd(), envPath) || envPath}.`,
268
+ );
269
+ if (changed.length > 0) console.log(`Changed: ${changed.join(", ")}`);
270
+ if (skipped.length > 0) {
271
+ console.log(
272
+ `Kept existing values: ${skipped.join(", ")} (use --force to overwrite)`,
273
+ );
274
+ }
275
+ console.log("");
276
+ console.log("Next steps:");
277
+ console.log("1. Sign in as WORKSPACE_OWNER_EMAIL.");
278
+ console.log("2. Create or select the org named by WORKSPACE_ORG_NAME.");
279
+ console.log("3. Set the org allowed domain to WORKSPACE_ORG_DOMAIN.");
280
+ console.log("4. Set or sync the org A2A secret from A2A_SECRET across apps.");
281
+ }
282
+
283
+ main();