@tryvitris/website-harness 0.1.0-beta.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/agent/refs/admin.md +11 -0
- package/agent/refs/launch.md +13 -11
- package/bin/api/cli/api.js +3 -3
- package/bin/api/cli.js +4 -4
- package/bin/chunks/{chunk-FEFLSJM5.js → chunk-LZC37OJZ.js} +1 -1
- package/bin/chunks/{chunk-QQOMS2DL.js → chunk-SEMH6MCV.js} +121 -15
- package/bin/chunks/{chunk-DYKLM4I3.js → chunk-UFTVWTEZ.js} +22 -4
- package/bin/chunks/{chunk-WNE543KF.js → chunk-YXJAQYDE.js} +1 -1
- package/bin/cloudflare/backend.js +143 -61
- package/bin/types/src/admin/connection.d.ts +3 -0
- package/bin/types/src/cli/admin.d.ts +21 -0
- package/bin/types/src/cli/site-create.d.ts +2 -0
- package/bin/vitris-create-site.js +3 -1
- package/bin/vitris-go.js +1 -1
- package/bin/vitris.js +4 -4
- package/docs/ADMIN.md +85 -0
- package/docs/CLI.md +11 -5
- package/docs/CLOUDFLARE.md +24 -2
- package/docs/DISTRIBUTION.md +5 -3
- package/docs/PRESERVE.md +1 -1
- package/docs/SECURITY.md +2 -2
- package/package.json +28 -23
- package/templates/site/AGENTS.md +3 -1
- package/templates/site/README.md +5 -3
- package/templates/site/deployment/README.md +24 -0
- package/templates/site/{.github/workflows/deploy.yml → deployment/github-actions.yml.example} +5 -6
- package/templates/site/gitignore +3 -0
- package/templates/site/package.json +2 -1
package/README.md
CHANGED
|
@@ -6,8 +6,10 @@ The package is [`@tryvitris/website-harness`](https://www.npmjs.com/package/@try
|
|
|
6
6
|
|
|
7
7
|
## Create a website
|
|
8
8
|
|
|
9
|
+
An optional separate business admin is available through `vitris admin setup|status|verify`; new-site `--admin` prepares follow-up instructions. See the [admin integration guide](docs/ADMIN.md) for pinned packages, local plan/apply, backend grants and the separate customer-account Cloudflare rollout.
|
|
10
|
+
|
|
9
11
|
```bash
|
|
10
|
-
npx --package @tryvitris/website-harness@
|
|
12
|
+
npx --package @tryvitris/website-harness@1.0.0 vitris site create \
|
|
11
13
|
--dir acme --slug acme --name "Acme Shop" \
|
|
12
14
|
--from https://old-site.example --template "Still & Field" \
|
|
13
15
|
--content-file ./acme-notes.md
|
|
@@ -50,6 +52,8 @@ For example, ask your agent to “improve the mobile layout,” “review the vi
|
|
|
50
52
|
|
|
51
53
|
## Develop the harness
|
|
52
54
|
|
|
55
|
+
Customer sites build and verify locally, then upload the exact finished bundle directly to Cloudflare Pages. Use `npm run deploy:preview` for a hosted preview and `npm run deploy` for explicitly requested production publication. GitHub stores source; hosted deployment automation is opt-in. See [Cloudflare setup](docs/CLOUDFLARE.md) for new sites and migration of existing sites.
|
|
56
|
+
|
|
53
57
|
```bash
|
|
54
58
|
npm ci
|
|
55
59
|
npx playwright install chromium
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Optional business admin
|
|
2
|
+
|
|
3
|
+
Only prepare an admin when requested or when the operator opts in with site creation `--admin`. Continue the website's prepare → CURRENT → work → complete workflow and verified local-preview stop. Admin rollout does not bypass website consent or release gates.
|
|
4
|
+
|
|
5
|
+
Read the installed harness's `docs/ADMIN.md` and the published admin's packaged `agent/WORKFLOW.md`, `agent/refs/harness.md`, and `agent/refs/cloudflare.md`. Use the pinned **0.1.0-alpha.1** release. Run `vitris admin setup --site WEBSITE --dir SIBLING_ADMIN --domain admin.customer-domain.com --website HTTPS_SITE_ORIGIN --tenant EXISTING_TENANT_ID --backend-origin HTTPS_BACKEND --plan`. Review `artifacts/admin/setup-plan.json`, then repeat with `--apply` and a dedicated environment `VITRIS_ADMIN_API_TOKEN`. Reuse the published installer; never reproduce it or silently overwrite an installation.
|
|
6
|
+
|
|
7
|
+
Setup is local scaffolding and public connection metadata only. Preserve forms, origins, tenants, existing accounts/records, authored source and workflow progress. Never infer compatibility from scaffolding: an older deployed backend needs its own reviewed API upgrade. `vitris admin status` is local metadata; `vitris admin verify --site WEBSITE --dir ADMIN` checks deployed identity and authoritative reads with the dedicated credential, without printing records. Failures must remain visible, with no demo fallback.
|
|
8
|
+
|
|
9
|
+
Default scopes: leads:read and analytics:read. Add --lead-write to both setup commands only for explicit status-write authorization. Prepare a complete hashed grant map with `vitris admin grant`; preserve unrelated grants. Store only SHA-256 in backend ADMIN_CONNECTIONS, and upload the raw token to the admin as HARNESS_API_TOKEN through secret stdin. Never commit configuration or credentials.
|
|
10
|
+
|
|
11
|
+
Prepare explicit, separate customer-account provisioning, D1 migrations, owner bootstrap, Cloudflare Email Service onboarding, authentication, DNS/TLS and deployment steps. Never deploy/migrate a shared backend as a side effect. Sites without collection need an explicitly provisioned backend and separately reviewed form/analytics activation; they have no historical data to import. Production demo login is forbidden. Test submissions and notification captures remain isolated and read-only; preview traffic is excluded from analytics. Clearly label synthetic fixtures and stub services. Pages, Media, page builders and arbitrary HTML editing remain outside scope.
|
package/agent/refs/launch.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
# Launch (optional)
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
# Launch (optional)
|
|
2
|
+
|
|
3
|
+
For a separately requested business admin at `admin.customer-domain.com`, read `admin.md` and the installed harness's `docs/ADMIN.md`. Use `vitris admin setup --plan` and reviewed `--apply`; verify the actual deployed backend with `vitris admin verify`. Local scaffolding does not enable collection/authentication or deploy infrastructure. Keep the sibling admin's Cloudflare provisioning and release separate from website publication.
|
|
4
|
+
|
|
5
|
+
Only when the operator asks after `preview_ready`. Build and verify locally, then upload the exact staged bundle directly to Cloudflare Pages. GitHub stores source and history; hosted deployment automation is opt-in.
|
|
6
|
+
|
|
7
|
+
1. Commit authored source and the lockfile to the customer's private repository when requested. Keep `dist/`, `agent/release-bundles/`, `agent/release-candidate.json`, `agent/releases/`, captures and credentials out of Git.
|
|
8
|
+
2. Authenticate with the operator's own `npx wrangler login` and confirm the account. For a new site create a Pages Direct Upload project with `npx wrangler pages project create PROJECT`. Set `launch.json.pagesProject` and the real production `launch.json.publicOrigin`. Install Chromium locally for preflight. Run `npm run deploy:preview` and accept the hosted preview; run `npm run deploy` only for explicit production publication. Ordinary deployment builds and verifies locally, stages the exact checked files, uploads them, records their identity and checks the hosted URL. Preservation sites support temporary previews only.
|
|
9
|
+
3. For existing Git-connected Pages projects, disable automatic production and preview branch deployments before local uploads. Review and disable existing automatic GitHub deployment workflows as well. Preserve project names, domains, launch configuration, authored files and workflow progress. Installing or upgrading the harness does not migrate package scripts or remote settings. See the installed harness's `docs/CLOUDFLARE.md`.
|
|
10
|
+
4. Backend tenant registration, contact forms, administration sign-in, custom domains and DNS are separate configured features. If publishing through the authenticated backend Worker, retain the exact verified bundle signing and attestation contract and review that path before migration. Direct Pages uploads do not provision tenants or forms.
|
|
11
|
+
5. Hosted automation is opt-in. `deployment/github-actions.yml.example` is inactive until an operator chooses and installs it under `.github/workflows/` with protected secrets. Cloudflare Git builds are another optional alternative. Enable one publication path per site; preserve preflight and bundle integrity checks in every ordinary path.
|
|
12
|
+
|
|
13
|
+
Do not merge secrets into git. Use `.env` for Cloudflare tokens.
|
|
12
14
|
|
|
13
15
|
For an explicitly requested quick publish, use `vitris deploy --quick --environment production` (`npm run deploy:quick` in newly scaffolded sites). This rebuilds and deploys without browser, graph or launch review gates. Compilation, Cloudflare credentials, staged-file integrity and an HTTP smoke check still run. Receipts record build-only verification. Preservation sites use `--environment preview` and also skip screenshot comparisons. Ordinary deployment retains full preflight.
|
package/bin/api/cli/api.js
CHANGED
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
runDoctor,
|
|
12
12
|
shareSite,
|
|
13
13
|
synthesizeSite
|
|
14
|
-
} from "../../chunks/chunk-
|
|
14
|
+
} from "../../chunks/chunk-YXJAQYDE.js";
|
|
15
15
|
import {
|
|
16
16
|
applyOperations,
|
|
17
17
|
buildSiteDraft,
|
|
@@ -31,14 +31,14 @@ import {
|
|
|
31
31
|
} from "../../chunks/chunk-A2XMAPVW.js";
|
|
32
32
|
import {
|
|
33
33
|
createSite
|
|
34
|
-
} from "../../chunks/chunk-
|
|
34
|
+
} from "../../chunks/chunk-UFTVWTEZ.js";
|
|
35
35
|
import "../../chunks/chunk-2NW7KSPA.js";
|
|
36
36
|
import {
|
|
37
37
|
buildAdapterConfig,
|
|
38
38
|
runPreserve,
|
|
39
39
|
runSite,
|
|
40
40
|
statusSite
|
|
41
|
-
} from "../../chunks/chunk-
|
|
41
|
+
} from "../../chunks/chunk-LZC37OJZ.js";
|
|
42
42
|
import "../../chunks/chunk-R2WRQDVE.js";
|
|
43
43
|
import "../../chunks/chunk-34H54KEY.js";
|
|
44
44
|
import {
|
package/bin/api/cli.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runCli
|
|
3
|
-
} from "../chunks/chunk-
|
|
4
|
-
import "../chunks/chunk-
|
|
3
|
+
} from "../chunks/chunk-SEMH6MCV.js";
|
|
4
|
+
import "../chunks/chunk-YXJAQYDE.js";
|
|
5
5
|
import "../chunks/chunk-FGSQE3DU.js";
|
|
6
6
|
import "../chunks/chunk-WMJVLMU2.js";
|
|
7
7
|
import "../chunks/chunk-A2XMAPVW.js";
|
|
8
|
-
import "../chunks/chunk-
|
|
8
|
+
import "../chunks/chunk-UFTVWTEZ.js";
|
|
9
9
|
import "../chunks/chunk-2NW7KSPA.js";
|
|
10
10
|
import "../chunks/chunk-3OKR5AZJ.js";
|
|
11
|
-
import "../chunks/chunk-
|
|
11
|
+
import "../chunks/chunk-LZC37OJZ.js";
|
|
12
12
|
import "../chunks/chunk-R2WRQDVE.js";
|
|
13
13
|
import "../chunks/chunk-34H54KEY.js";
|
|
14
14
|
import "../chunks/chunk-JCRGE6AL.js";
|
|
@@ -253,7 +253,7 @@ function scaffoldNestedSite(opts) {
|
|
|
253
253
|
for (const [key, value] of Object.entries(pkg.scripts)) pkg.scripts[key] = value.replaceAll("__SLUG__", opts.slug);
|
|
254
254
|
writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
255
255
|
`);
|
|
256
|
-
for (const file2 of ["site.json", "README.md", "AGENTS.md"]) {
|
|
256
|
+
for (const file2 of ["site.json", "README.md", "AGENTS.md", "deployment/github-actions.yml.example"]) {
|
|
257
257
|
const p = join2(dest, file2);
|
|
258
258
|
if (!existsSync2(p)) continue;
|
|
259
259
|
if (file2 === "site.json") {
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
runDoctor,
|
|
12
12
|
shareSite,
|
|
13
13
|
synthesizeSite
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-YXJAQYDE.js";
|
|
15
15
|
import {
|
|
16
16
|
verifySite
|
|
17
17
|
} from "./chunk-WMJVLMU2.js";
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "./chunk-A2XMAPVW.js";
|
|
21
21
|
import {
|
|
22
22
|
createSite
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-UFTVWTEZ.js";
|
|
24
24
|
import {
|
|
25
25
|
explainStage
|
|
26
26
|
} from "./chunk-3OKR5AZJ.js";
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
runSite,
|
|
29
29
|
statusSite,
|
|
30
30
|
workflowArgvFromFlags
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-LZC37OJZ.js";
|
|
32
32
|
import {
|
|
33
33
|
EXIT_CODES
|
|
34
34
|
} from "./chunk-JCRGE6AL.js";
|
|
@@ -50,9 +50,9 @@ import {
|
|
|
50
50
|
} from "./chunk-T6PE4KN4.js";
|
|
51
51
|
|
|
52
52
|
// src/cli/index.ts
|
|
53
|
-
import { existsSync as existsSync2, readFileSync as
|
|
54
|
-
import { resolve as
|
|
55
|
-
import { spawnSync } from "node:child_process";
|
|
53
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
54
|
+
import { resolve as resolve3 } from "node:path";
|
|
55
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
56
56
|
|
|
57
57
|
// src/cli/events.ts
|
|
58
58
|
var EventStream = class {
|
|
@@ -270,6 +270,106 @@ async function formsCommand(command, opts) {
|
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
// src/cli/admin.ts
|
|
274
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
275
|
+
import { resolve as resolve2 } from "node:path";
|
|
276
|
+
import { createRequire } from "node:module";
|
|
277
|
+
import { spawnSync } from "node:child_process";
|
|
278
|
+
import { parseManifest } from "@tryvitris/website-admin-contracts";
|
|
279
|
+
|
|
280
|
+
// src/admin/connection.ts
|
|
281
|
+
async function readConnection(manifest, token, resource, test = false, transport = fetch) {
|
|
282
|
+
if (!manifest.backend || !/^[a-zA-Z0-9_-]{32,200}$/.test(token)) throw Error("A dedicated VITRIS_ADMIN_API_TOKEN and backend connection are required");
|
|
283
|
+
const url = new URL(`/api/admin/v1/${encodeURIComponent(manifest.backend.tenantId)}/${resource}`, manifest.backend.origin);
|
|
284
|
+
if (test) url.searchParams.set("mode", "test");
|
|
285
|
+
const response = await transport(url, { redirect: "manual", signal: AbortSignal.timeout(12e3), headers: { authorization: `Bearer ${token}`, "x-vitris-installation": manifest.installationId, "x-vitris-site-origin": manifest.site.origin } });
|
|
286
|
+
if (!response.ok) throw Error(`Admin v1 ${resource} rejected (${response.status}); connection is not verified`);
|
|
287
|
+
const reader = response.body?.getReader();
|
|
288
|
+
if (!reader) throw Error("Empty backend response");
|
|
289
|
+
const chunks = [];
|
|
290
|
+
let size = 0;
|
|
291
|
+
for (; ; ) {
|
|
292
|
+
const { done, value } = await reader.read();
|
|
293
|
+
if (done) break;
|
|
294
|
+
size += value.length;
|
|
295
|
+
if (size > 4e6) {
|
|
296
|
+
await reader.cancel();
|
|
297
|
+
throw Error("Backend response too large");
|
|
298
|
+
}
|
|
299
|
+
chunks.push(value);
|
|
300
|
+
}
|
|
301
|
+
const bytes = new Uint8Array(size);
|
|
302
|
+
let offset = 0;
|
|
303
|
+
for (const chunk of chunks) {
|
|
304
|
+
bytes.set(chunk, offset);
|
|
305
|
+
offset += chunk.length;
|
|
306
|
+
}
|
|
307
|
+
const data = JSON.parse(new TextDecoder().decode(bytes));
|
|
308
|
+
if (data?.schemaVersion !== 1 || data.installationId !== manifest.installationId || data.tenantId !== manifest.backend.tenantId || data.siteOrigin !== manifest.site.origin) throw Error("Backend contract or installation identity mismatch");
|
|
309
|
+
if (resource === "status") {
|
|
310
|
+
if (!Array.isArray(data.scopes)) throw Error("Invalid backend scopes");
|
|
311
|
+
} else {
|
|
312
|
+
if (data.source !== "connected" || !Array.isArray(data.rows) || data.rows.length > 200) throw Error("Invalid backend records");
|
|
313
|
+
if (resource === "leads" && data.rows.some((r) => ["id", "name", "email", "message", "created_at", "status"].some((k) => typeof r?.[k] !== "string"))) throw Error("Invalid lead response");
|
|
314
|
+
if (resource === "analytics" && data.rows.some((r) => typeof r?.day !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(r.day) || ["views", "visitors", "enquiries"].some((k) => typeof r[k] !== "number" || !Number.isFinite(r[k]) || r[k] < 0))) throw Error("Invalid analytics response");
|
|
315
|
+
}
|
|
316
|
+
return data;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/cli/admin.ts
|
|
320
|
+
function mergeAdminGrant(existing, addition) {
|
|
321
|
+
const object = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
322
|
+
if (!object(existing) || !object(addition) || Object.keys(addition).length !== 1) throw Error("Expected complete grant map and one installation grant");
|
|
323
|
+
for (const [id, raw] of Object.entries({ ...existing, ...addition })) {
|
|
324
|
+
const g = raw;
|
|
325
|
+
if (!/^[a-z][a-z0-9-]{0,50}$/.test(id) || !object(g) || typeof g.tenantId !== "string" || !/^[a-zA-Z0-9_-]{1,100}$/.test(g.tenantId) || typeof g.tokenHash !== "string" || !/^[a-f0-9]{64}$/.test(g.tokenHash) || typeof g.siteOrigin !== "string" || new URL(g.siteOrigin).origin !== g.siteOrigin || !g.siteOrigin.startsWith("https://") || !Array.isArray(g.scopes) || g.scopes.some((s) => !["leads:read", "analytics:read", "leads:write"].includes(s)) || Object.keys(g).some((k) => !["tenantId", "siteOrigin", "tokenHash", "scopes"].includes(k))) throw Error("Invalid hashed grant; raw credentials are forbidden");
|
|
326
|
+
}
|
|
327
|
+
for (const id of Object.keys(addition)) if (Object.hasOwn(existing, id) && JSON.stringify(existing[id]) !== JSON.stringify(addition[id])) throw Error("Grant already exists with different authority; rotate explicitly");
|
|
328
|
+
return { ...existing, ...addition };
|
|
329
|
+
}
|
|
330
|
+
async function adminCommand(command, options) {
|
|
331
|
+
try {
|
|
332
|
+
const site = resolve2(options.site || ".");
|
|
333
|
+
if (command === "grant") {
|
|
334
|
+
if (!options.dir || !options.grants || !options.out || options.dryRun) throw Error("admin grant requires --dir, --grants COMPLETE_MAP_FILE and --out NEW_IGNORED_FILE");
|
|
335
|
+
const addition = JSON.parse(readFileSync2(resolve2(options.dir, ".vitris/backend-grant.json"), "utf8"));
|
|
336
|
+
const manifest2 = parseManifest(JSON.parse(readFileSync2(resolve2(options.dir, "admin.config.json"), "utf8")));
|
|
337
|
+
const grant = addition[manifest2.installationId];
|
|
338
|
+
if (!grant || grant.tenantId !== manifest2.backend?.tenantId || grant.siteOrigin !== manifest2.site.origin) throw Error("Grant and installation identity differ");
|
|
339
|
+
const merged = mergeAdminGrant(JSON.parse(readFileSync2(resolve2(options.grants), "utf8")), addition);
|
|
340
|
+
writeFileSync2(resolve2(options.out), JSON.stringify(merged, null, 2) + "\n", { flag: "wx", mode: 384 });
|
|
341
|
+
return { ok: true, exitCode: 0, summary: "Merged hashed grants into the new local file. No backend secret changed; review and install explicitly through secret stdin." };
|
|
342
|
+
}
|
|
343
|
+
if (command === "setup") {
|
|
344
|
+
if (!options.dir || !options.domain || !options.tenant || !!options.plan === !!options.apply) throw Error("admin setup requires --dir, --domain, --tenant and exactly one of --plan or --apply");
|
|
345
|
+
if (options.dryRun) throw Error("Use --plan for local setup planning; --dry-run does not write a setup plan");
|
|
346
|
+
const args = ["setup", "--site", site, "--dir", resolve2(options.dir), "--domain", options.domain, "--tenant", options.tenant, options.apply ? "--apply" : "--plan"];
|
|
347
|
+
if (options.website) args.push("--website", options.website);
|
|
348
|
+
if (options.backendOrigin) args.push("--backend-origin", options.backendOrigin);
|
|
349
|
+
if (options.leadWrite) args.push("--lead-write");
|
|
350
|
+
const entry = createRequire(import.meta.url).resolve("@tryvitris/website-admin-harness/cli");
|
|
351
|
+
const output = spawnSync(process.execPath, [entry, ...args], { encoding: "utf8", windowsHide: true, maxBuffer: 1024 * 1024 });
|
|
352
|
+
if (output.status !== 0) throw Error(output.error?.message || output.stderr.trim() || "Admin setup failed");
|
|
353
|
+
return { ok: true, exitCode: 0, summary: output.stdout.trim() };
|
|
354
|
+
}
|
|
355
|
+
if (!["status", "verify"].includes(command)) throw Error("Use vitris admin setup|status|verify|grant");
|
|
356
|
+
const launch = JSON.parse(readFileSync2(resolve2(site, "launch.json"), "utf8"));
|
|
357
|
+
if (command === "status") return { ok: true, exitCode: 0, summary: launch.admin ? "Local admin metadata exists. Backend, authentication and deployment are unverified; run admin verify with --dir." : "No admin connection. Read docs/ADMIN.md; scaffolding alone does not enable collection.", data: { connection: launch.admin || null, verified: false } };
|
|
358
|
+
if (!options.dir) throw Error("admin verify requires --dir ADMIN_DIRECTORY");
|
|
359
|
+
const manifest = parseManifest(JSON.parse(readFileSync2(resolve2(options.dir, "admin.config.json"), "utf8")));
|
|
360
|
+
if (!launch.admin || manifest.installationId !== launch.admin.installationId || manifest.site.id !== launch.admin.siteId || manifest.adminOrigin !== launch.admin.url || manifest.site.origin !== launch.admin.siteOrigin || manifest.backend?.tenantId !== launch.admin.tenantId) throw Error("Website and admin installation identities differ");
|
|
361
|
+
const token = process.env.VITRIS_ADMIN_API_TOKEN || "";
|
|
362
|
+
const status = await readConnection(manifest, token, "status");
|
|
363
|
+
if (!["leads:read", "analytics:read"].every((scope) => status.scopes.includes(scope))) throw Error("Default read scopes are missing");
|
|
364
|
+
const leads = await readConnection(manifest, token, "leads");
|
|
365
|
+
const tests = await readConnection(manifest, token, "leads", true);
|
|
366
|
+
const analytics = await readConnection(manifest, token, "analytics");
|
|
367
|
+
return { ok: true, exitCode: 0, summary: "Admin v1 data connection verified. Authentication, collection and deployment require separate acceptance checks.", data: { verified: true, productionLeads: leads.rows.length, testSubmissions: tests.rows.length, analyticsDays: analytics.rows.length, scopes: status.scopes } };
|
|
368
|
+
} catch (error) {
|
|
369
|
+
return { ok: false, exitCode: EXIT_CODES.PRECONDITION_FAILED, error: error instanceof Error ? error.message : String(error) };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
273
373
|
// src/cli/index.ts
|
|
274
374
|
function parseArgs(args) {
|
|
275
375
|
const command = [];
|
|
@@ -309,9 +409,9 @@ function parseArgs(args) {
|
|
|
309
409
|
}
|
|
310
410
|
function resolvePrecedence(flags, siteRoot) {
|
|
311
411
|
const merged = {};
|
|
312
|
-
if (siteRoot && existsSync2(
|
|
412
|
+
if (siteRoot && existsSync2(resolve3(siteRoot, "launch.json"))) {
|
|
313
413
|
try {
|
|
314
|
-
const launch = JSON.parse(
|
|
414
|
+
const launch = JSON.parse(readFileSync3(resolve3(siteRoot, "launch.json"), "utf8"));
|
|
315
415
|
if (typeof launch.ownerEmail === "string") merged.owner = launch.ownerEmail;
|
|
316
416
|
if (typeof launch.slug === "string") merged.slug = launch.slug;
|
|
317
417
|
if (typeof launch.name === "string") merged.name = launch.name;
|
|
@@ -320,9 +420,9 @@ function resolvePrecedence(flags, siteRoot) {
|
|
|
320
420
|
}
|
|
321
421
|
if (flags.config && typeof flags.config === "string") {
|
|
322
422
|
try {
|
|
323
|
-
const configPath =
|
|
423
|
+
const configPath = resolve3(flags.config);
|
|
324
424
|
if (existsSync2(configPath)) {
|
|
325
|
-
const cfg = JSON.parse(
|
|
425
|
+
const cfg = JSON.parse(readFileSync3(configPath, "utf8"));
|
|
326
426
|
for (const [k, v] of Object.entries(cfg)) {
|
|
327
427
|
if (typeof v === "string" || typeof v === "boolean") merged[k] = v;
|
|
328
428
|
}
|
|
@@ -354,7 +454,7 @@ async function runCli(argv) {
|
|
|
354
454
|
}
|
|
355
455
|
const stream = new EventStream(outputMode);
|
|
356
456
|
if (flags.version || flags.v) {
|
|
357
|
-
stream.log(`@tryvitris/website-harness v${JSON.parse(
|
|
457
|
+
stream.log(`@tryvitris/website-harness v${JSON.parse(readFileSync3(resolve3(findHarnessRoot(), "package.json"), "utf8")).version}`);
|
|
358
458
|
return EXIT_CODES.SUCCESS;
|
|
359
459
|
}
|
|
360
460
|
if (flags.help || flags.h || command.length === 0) {
|
|
@@ -363,6 +463,7 @@ async function runCli(argv) {
|
|
|
363
463
|
Usage: vitris <command> [options]
|
|
364
464
|
|
|
365
465
|
Commands:
|
|
466
|
+
admin setup|status|verify Separate business admin; explicit cloud rollout
|
|
366
467
|
forms inspect|setup|status Inventory, provision, and check contact forms
|
|
367
468
|
doctor Check environment, dependencies, and site health
|
|
368
469
|
site create --dir DIR --slug SLUG Scaffold a new customer site repo (--from URL or --blank)
|
|
@@ -391,6 +492,7 @@ Options:
|
|
|
391
492
|
--template-release RELEASE Optional template revision
|
|
392
493
|
--content TEXT Additional business content or instructions
|
|
393
494
|
--content-file FILE UTF-8 content file (instead of --content)
|
|
495
|
+
--admin New-site opt-in: prepare sibling admin next steps
|
|
394
496
|
--backend-origin URL Backend origin for a new site
|
|
395
497
|
--site DIR Site directory path (default: current directory)
|
|
396
498
|
--json Output single structured JSON result at completion
|
|
@@ -413,7 +515,10 @@ Options:
|
|
|
413
515
|
const secondary = command[1];
|
|
414
516
|
let result;
|
|
415
517
|
try {
|
|
416
|
-
if (primary === "
|
|
518
|
+
if (primary === "admin") {
|
|
519
|
+
const value = (key) => typeof opts[key] === "string" ? opts[key] : void 0;
|
|
520
|
+
result = await adminCommand(secondary || "status", { site: siteRoot, dir: value("dir"), domain: value("domain"), website: value("website"), tenant: value("tenant"), backendOrigin: value("backend-origin"), plan: !!opts.plan, apply: !!opts.apply, leadWrite: !!opts["lead-write"], grants: value("grants"), out: value("out"), dryRun });
|
|
521
|
+
} else if (primary === "forms") {
|
|
417
522
|
const list = (name) => typeof opts[name] === "string" ? opts[name].split(",").map((s) => s.trim()).filter(Boolean) : void 0;
|
|
418
523
|
result = await formsCommand(secondary || "status", { site: siteRoot, tenant: typeof opts.tenant === "string" ? opts.tenant : void 0, backendOrigin: typeof opts["backend-origin"] === "string" ? opts["backend-origin"] : void 0, origins: list("origins"), recipients: list("recipients"), mode: typeof opts.mode === "string" ? opts.mode : void 0, apply: !!opts.apply, testKeys: !!opts["test-keys"], cloudflare: !!opts.cloudflare, worker: typeof opts.worker === "string" ? opts.worker : void 0, config: typeof opts["forms-config"] === "string" ? opts["forms-config"] : void 0, dryRun });
|
|
419
524
|
} else if (primary === "doctor") {
|
|
@@ -425,6 +530,7 @@ Options:
|
|
|
425
530
|
} else if (primary === "site" && secondary === "create") {
|
|
426
531
|
result = await createSite({
|
|
427
532
|
...siteInputsFromFlags(opts),
|
|
533
|
+
admin: !!opts.admin,
|
|
428
534
|
dir: typeof opts.dir === "string" ? opts.dir : "",
|
|
429
535
|
slug: typeof opts.slug === "string" ? opts.slug : "",
|
|
430
536
|
name: typeof opts.name === "string" ? opts.name : void 0,
|
|
@@ -559,14 +665,14 @@ Options:
|
|
|
559
665
|
if (sub === "doctor") {
|
|
560
666
|
result = await runDoctor({ provider: "cloudflare", operation: "pages" });
|
|
561
667
|
} else if (sub === "whoami") {
|
|
562
|
-
const probe =
|
|
668
|
+
const probe = spawnSync2(process.platform === "win32" ? "npx.cmd" : "npx", ["wrangler", "whoami"], { encoding: "utf8" });
|
|
563
669
|
result = {
|
|
564
670
|
ok: probe.status === 0,
|
|
565
671
|
exitCode: probe.status === 0 ? EXIT_CODES.SUCCESS : EXIT_CODES.CONFIG_OR_CLI_ERROR,
|
|
566
672
|
summary: probe.stdout || probe.stderr
|
|
567
673
|
};
|
|
568
674
|
} else if (sub === "login") {
|
|
569
|
-
const probe =
|
|
675
|
+
const probe = spawnSync2(process.platform === "win32" ? "npx.cmd" : "npx", ["wrangler", "login"], { stdio: "inherit" });
|
|
570
676
|
result = {
|
|
571
677
|
ok: probe.status === 0,
|
|
572
678
|
exitCode: probe.status === 0 ? EXIT_CODES.SUCCESS : EXIT_CODES.GENERIC_ERROR,
|
|
@@ -580,7 +686,7 @@ Options:
|
|
|
580
686
|
};
|
|
581
687
|
}
|
|
582
688
|
} else if (primary === "knowledge" && secondary === "explain") {
|
|
583
|
-
const explained = explainStage(
|
|
689
|
+
const explained = explainStage(resolve3(typeof opts.site === "string" ? opts.site : "."), typeof opts.stage === "string" ? opts.stage : "direction");
|
|
584
690
|
result = { ok: true, exitCode: EXIT_CODES.SUCCESS, summary: explained.text, data: explained };
|
|
585
691
|
} else if (primary === "share") {
|
|
586
692
|
result = await shareSite({
|
|
@@ -151,7 +151,9 @@ Preserved custom website from ${fromArg}.
|
|
|
151
151
|
2. npx playwright install chromium
|
|
152
152
|
3. npm run go
|
|
153
153
|
4. npm run preview
|
|
154
|
-
5. npm run deploy (temporary Cloudflare preview)
|
|
154
|
+
5. npm run deploy (build and verify locally, then upload a temporary Cloudflare preview)
|
|
155
|
+
|
|
156
|
+
Local builds and direct uploads are the default. GitHub stores source; no deployment workflow runs automatically. Keep dist/ and release bundles out of Git.
|
|
155
157
|
|
|
156
158
|
Custom source: preserved/. Verification: agent/reviews/preserve.json. See AGENTS.md and the installed harness docs/PRESERVE.md. Run vitris forms inspect and vitris forms setup to register and connect contact forms; backendOrigin alone does not enable submission or notifications. See docs/CONTACT-FORMS.md.
|
|
157
159
|
`);
|
|
@@ -161,10 +163,14 @@ Run npm run go to capture and verify the existing client-facing site. Keep its a
|
|
|
161
163
|
|
|
162
164
|
Repair source in preserved/ when agent/reviews/preserve.json reports a failure; run npm run go again. Never change reference screenshots or hashes to make a check pass. Do not overwrite source by recapturing. onboarding/preserve.json records the source and captured routes. Contact forms require explicit registration and public launch.json forms bindings. backendOrigin alone does not enable delivery. Default previews show a notice without submitting. See docs/CONTACT-FORMS.md.
|
|
163
165
|
|
|
164
|
-
Use npm run deploy
|
|
166
|
+
Use npm run deploy to build and verify locally and directly upload a temporary Cloudflare preview. Keep dist/ and release bundles out of Git. Hosted automation is opt-in; deployment/github-actions.yml.example is inactive by default. Production and customer domains are outside this mode. Scaffold a later redesign separately from the same source URL.
|
|
165
167
|
`);
|
|
166
168
|
}
|
|
167
|
-
|
|
169
|
+
if (mode === "preserve") {
|
|
170
|
+
const instructions = join(dest, "AGENTS.md");
|
|
171
|
+
writeFileSync(instructions, readFileSync(instructions, "utf8") + "\nFor a requested sibling business admin, read agent/refs/admin.md and the installed harness docs/ADMIN.md. Use vitris admin setup --plan then reviewed --apply. Preserve current forms and backend identity; cloud provisioning, authentication and deployment remain separate.\n");
|
|
172
|
+
}
|
|
173
|
+
const workflow = join(dest, "deployment", "github-actions.yml.example");
|
|
168
174
|
if (existsSync(workflow)) {
|
|
169
175
|
if (mode === "preserve") {
|
|
170
176
|
writeFileSync(workflow, `name: Preserve preview
|
|
@@ -198,6 +204,18 @@ jobs:
|
|
|
198
204
|
copyMarkdown: gathered.copyMarkdown
|
|
199
205
|
});
|
|
200
206
|
persistSiteInputs(dest, inputs);
|
|
207
|
+
if (opts.admin) {
|
|
208
|
+
writeFileSync(join(dest, "ADMIN-SETUP.md"), `# Optional business admin
|
|
209
|
+
|
|
210
|
+
Read node_modules/@tryvitris/website-harness/docs/ADMIN.md and agent/refs/admin.md.
|
|
211
|
+
|
|
212
|
+
Prepare a separate sibling installation after confirming the exact website origin, existing tenant ID and backend origin:
|
|
213
|
+
|
|
214
|
+
vitris admin setup --site . --dir ../${slug}-admin --domain admin.CUSTOMER_DOMAIN --website https://www.CUSTOMER_DOMAIN --tenant EXISTING_TENANT_ID --backend-origin https://BACKEND --plan
|
|
215
|
+
|
|
216
|
+
Review artifacts/admin/setup-plan.json, then repeat with --apply and a dedicated VITRIS_ADMIN_API_TOKEN in the operator environment. This opt-in has created instructions only. No backend, grant, account, collection, authentication, DNS or cloud resources have been changed.
|
|
217
|
+
`);
|
|
218
|
+
}
|
|
201
219
|
const graphPath = join(dest, "agent", "graph.yaml");
|
|
202
220
|
if (existsSync(graphPath)) {
|
|
203
221
|
const graph = parseGraphYaml(readFileSync(graphPath, "utf8"));
|
|
@@ -210,7 +228,7 @@ jobs:
|
|
|
210
228
|
return {
|
|
211
229
|
ok: true,
|
|
212
230
|
exitCode: EXIT_CODES.SUCCESS,
|
|
213
|
-
summary: `Created site ${slug} at ${dest}`,
|
|
231
|
+
summary: `Created site ${slug} at ${dest}${opts.admin ? "; optional admin next steps: ADMIN-SETUP.md" : ""}`,
|
|
214
232
|
data: { dest, slug, name }
|
|
215
233
|
};
|
|
216
234
|
}
|