@agent-native/core 0.7.56 → 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.
- package/dist/a2a/agent-card.d.ts +1 -1
- package/dist/a2a/agent-card.d.ts.map +1 -1
- package/dist/a2a/agent-card.js +30 -2
- package/dist/a2a/agent-card.js.map +1 -1
- package/dist/a2a/server.d.ts.map +1 -1
- package/dist/a2a/server.js +1 -1
- package/dist/a2a/server.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +2 -0
- package/dist/cli/create.js.map +1 -1
- package/dist/client/NewWorkspaceAppFlow.d.ts.map +1 -1
- package/dist/client/NewWorkspaceAppFlow.js +34 -38
- package/dist/client/NewWorkspaceAppFlow.js.map +1 -1
- package/dist/client/settings/VoiceTranscriptionSection.d.ts.map +1 -1
- package/dist/client/settings/VoiceTranscriptionSection.js +114 -2
- package/dist/client/settings/VoiceTranscriptionSection.js.map +1 -1
- package/dist/deploy/workspace-deploy.d.ts.map +1 -1
- package/dist/deploy/workspace-deploy.js +7 -20
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +17 -1
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +14 -5
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/adapters/slack.d.ts +2 -2
- package/dist/integrations/adapters/slack.js +20 -15
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/shared/workspace-app-id.d.ts +1 -1
- package/dist/shared/workspace-app-id.d.ts.map +1 -1
- package/dist/shared/workspace-app-id.js +2 -0
- package/dist/shared/workspace-app-id.js.map +1 -1
- package/dist/templates/workspace-root/.env.example +6 -0
- package/dist/templates/workspace-root/AGENTS.md +50 -0
- package/dist/templates/workspace-root/README.md +18 -1
- package/dist/templates/workspace-root/package.json +2 -0
- package/dist/templates/workspace-root/scripts/repair-workspace-org.ts +283 -0
- package/package.json +1 -1
- package/src/templates/workspace-root/.env.example +6 -0
- package/src/templates/workspace-root/AGENTS.md +50 -0
- package/src/templates/workspace-root/README.md +18 -1
- package/src/templates/workspace-root/package.json +2 -0
- 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
|
@@ -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,
|
|
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();
|