@rubytech/create-maxy-code 0.1.17 → 0.1.19
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/__tests__/cdp-port-no-silent-fallback.test.js +2 -2
- package/dist/__tests__/onboarding-state-readback.test.js +61 -0
- package/dist/index.js +42 -0
- package/dist/onboarding-readback.js +27 -0
- package/package.json +2 -2
- package/payload/platform/config/brand-registry.json +4 -4
- package/payload/platform/config/brand.json +4 -4
- package/payload/platform/plugins/cloudflare/scripts/reset-tunnel.sh +1 -1
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +1 -1
- package/payload/platform/plugins/docs/references/deployment.md +25 -2
- package/payload/platform/scripts/installer-device-verify.sh +2 -2
- package/payload/platform/scripts/redact-install-logs.sh +2 -2
- package/payload/platform/scripts/seed-neo4j.sh +35 -0
- package/payload/platform/scripts/verify-skill-tool-surface.sh +1 -1
- package/payload/platform/scripts/vnc.sh +2 -2
- package/payload/premium-plugins/real-agency/plugins/brochures/skills/brand-design/SKILL.md +1 -1
- package/payload/premium-plugins/real-agency/plugins/brochures/skills/make-brochure/SKILL.md +9 -9
- package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/SKILL.md +7 -7
- package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/references/build.md +5 -5
- package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/references/copy.md +3 -3
- package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/references/images.md +1 -1
- package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/references/{page-landing.md → index-landing.md} +14 -14
- package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/references/{page.html → index.html} +4 -4
- package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/references/placeholders.md +6 -6
- package/payload/server/package.json +1 -1
- package/payload/server/public/assets/{Checkbox-nrf4ISU0.js → Checkbox-BmlrSRF8.js} +1 -1
- package/payload/server/public/assets/{admin-BctPxbjB.js → admin-Drvl0Okz.js} +53 -53
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-DZI73ap7.js → architectureDiagram-Q4EWVU46-DU8MnqOj.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-BQQz9YNZ.js → blockDiagram-DXYQGD6D-DwBZc1na.js} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-gl8-XVLH.js → c4Diagram-AHTNJAMY-C9ZHreVX.js} +1 -1
- package/payload/server/public/assets/channel-Boz4UHN2.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-839etdKR.js → chunk-336JU56O-DNXqHk42.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-hyvaLE5p.js → chunk-426QAEUC-DOLKkQGn.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-DEIYWdpd.js → chunk-4TB4RGXK-l8iZx-gQ.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-zWevOyjc.js → chunk-5FUZZQ4R-DQwRtUdO.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-CL9KWSZH.js → chunk-5PVQY5BW-CvvoBJnn.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-D91vGp64.js → chunk-EDXVE4YY-DV1J4-LR.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-DApK4iuk.js → chunk-ENJZ2VHE-Co6qpIQl.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-BOjV-nTe.js → chunk-ICPOFSXX-9v9O_mwr.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-D3Cv5Z_Z.js → chunk-OYMX7WX6-D2X1NuKZ.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-C92E-iRU.js → chunk-U2HBQHQK-DmqWEsN2.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-DohG6qWK.js → chunk-X2U36JSP-C0b2a6Bq.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-CyeLVSjf.js → chunk-YZCP3GAM-BdBAquWg.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-D7R-lONY.js → chunk-ZZ45TVLE-C1AFspLM.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-CRA6E97A.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-VnieTvEm.js +1 -0
- package/payload/server/public/assets/clone-6YhZHo8b.js +1 -0
- package/payload/server/public/assets/{dagre-DjhovTZd.js → dagre-7eiQzAHj.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-DFeRlQuy.js → dagre-KV5264BT-a88ylEXY.js} +1 -1
- package/payload/server/public/assets/data-Dq_if_B4.js +1 -0
- package/payload/server/public/assets/{device-url-actions-CE3A1UDw.js → device-url-actions-_s-UgHa3.js} +1 -1
- package/payload/server/public/assets/{diagram-5BDNPKRD-DsmRGnVL.js → diagram-5BDNPKRD-XuINnSy-.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-CVSK-mLR.js → diagram-G4DWMVQ6-B93iSQ8a.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-DMN94Pe-.js → diagram-MMDJMWI5-Gcoj967k.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-DaD4mLMc.js → diagram-TYMM5635-C2BKnsAF.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-BUkZ2Iq1.js → erDiagram-SMLLAGMA-DwbOuflI.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-DW8DX_ge.js → flowDiagram-DWJPFMVM-CEExhdxC.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-DWaRL6__.js → ganttDiagram-T4ZO3ILL-kYNYeUK-.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-BaPTFtVx.js → gitGraphDiagram-UUTBAWPF-D3X2eJPE.js} +1 -1
- package/payload/server/public/assets/graph-CNFkzAKU.js +1 -0
- package/payload/server/public/assets/{graph-labels-BRtJE9AE.js → graph-labels-C6ZZPglH.js} +1 -1
- package/payload/server/public/assets/{graphlib-BUhb3hPU.js → graphlib-_S6i_Jn2.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-Ch6CE3GO.js → infoDiagram-42DDH7IO-CQx0vDcC.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-DApFr2KO.js → ishikawaDiagram-UXIWVN3A-0eZUgqUP.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-D72xl-VA.js → journeyDiagram-VCZTEJTY-D-B2Kd_J.js} +1 -1
- package/payload/server/public/assets/jsx-runtime-CtqEPPN5.css +1 -0
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-TuAvkCJU.js → kanban-definition-6JOO6SKY-BmLn-OEz.js} +1 -1
- package/payload/server/public/assets/{line-Cr3lHgh8.js → line-h25nWPBw.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-BvbEd4_6.js → mermaid-parser.core-CXXeaSZi.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-CSIEcw1L.js → mermaid.core-BjFfgEHL.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CNLvqgk-.js → mindmap-definition-QFDTVHPH-Dql4ILoK.js} +1 -1
- package/payload/server/public/assets/{page-B_80xGrM.js → page-CdWWweCx.js} +1 -1
- package/payload/server/public/assets/{page-BcnqM490.js → page-D20UnO_r.js} +1 -1
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-CSOsdFn6.js → pieDiagram-DEJITSTG-nYaoTCKZ.js} +1 -1
- package/payload/server/public/assets/{public-DGrCAqZN.js → public-CXgyLdJU.js} +3 -3
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-Bsxz9S58.js → quadrantDiagram-34T5L4WZ-XG8xivm9.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-BZRrlQlh.js → requirementDiagram-MS252O5E-C2GzfT-6.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-C-W-Az7g.js → sankeyDiagram-XADWPNL6-3aI78p2X.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-DCZPAj4-.js → sequenceDiagram-FGHM5R23-CvaNRYuS.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-C3wODMGb.js → stateDiagram-FHFEXIEX-DHT2Zyvj.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-YmtuS9BN.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-TRzSKinM.js → timeline-definition-GMOUNBTQ-DqPjDUku.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-DSKZVM8N.js → vennDiagram-DHZGUBPP-lQB1LgjH.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-BRXL08eb.js → wardleyDiagram-NUSXRM2D-sxSMxkwm.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-CgWMOf0l.js → xychartDiagram-5P7HB3ND-4-dvvfSD.js} +1 -1
- package/payload/server/public/brand-constants.json +1 -1
- package/payload/server/public/brand-defaults.css +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +91 -6
- package/payload/server/public/assets/channel-BIMyzzFT.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-BAfXNwa9.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-D5bE1GaW.js +0 -1
- package/payload/server/public/assets/clone-DOH_suVb.js +0 -1
- package/payload/server/public/assets/data-DzTaKq-r.js +0 -1
- package/payload/server/public/assets/graph-dgoq2zvY.js +0 -1
- package/payload/server/public/assets/jsx-runtime-BgXAk35j.css +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Ch6qbhpm.js +0 -1
- /package/payload/server/public/assets/{jsx-runtime-Bz7aoCi7.js → jsx-runtime-BSJRynxp.js} +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// silent-fallback-masks-root-cause violation. The four runtime sites that
|
|
3
3
|
// previously substituted `9222 + offset` for a missing brand.json.cdpPort
|
|
4
4
|
// (paths.ts, admin/mcp/index.ts, vnc.sh, test-laptop-vnc-boot.sh) plus the
|
|
5
|
-
// installer-side brand stamp at packages/create-maxy/src/index.ts have all
|
|
5
|
+
// installer-side brand stamp at packages/create-maxy-code/src/index.ts have all
|
|
6
6
|
// been swept to loud-fail. This test asserts the three greps from criterion
|
|
7
7
|
// 2 of the task brief return zero matches; reintroducing any silent fallback
|
|
8
8
|
// fails CI immediately.
|
|
@@ -19,7 +19,7 @@ import { resolve } from "node:path";
|
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
20
|
const SELF_DIR = fileURLToPath(new URL(".", import.meta.url));
|
|
21
21
|
// dist/__tests__/ → repo root is four levels up
|
|
22
|
-
// (packages/create-maxy/dist/__tests__ → packages/create-maxy → packages → repo).
|
|
22
|
+
// (packages/create-maxy-code/dist/__tests__ → packages/create-maxy-code → packages → repo).
|
|
23
23
|
const REPO_ROOT = resolve(SELF_DIR, "..", "..", "..", "..");
|
|
24
24
|
function grepReturns(pattern, includes) {
|
|
25
25
|
const args = ["-RnE", pattern];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Task 033 — install-invariant for OnboardingState seed.
|
|
2
|
+
//
|
|
3
|
+
// The installer re-reads Neo4j after seed-neo4j.sh to verify the
|
|
4
|
+
// OnboardingState node landed. The parser turns cypher-shell's plain-
|
|
5
|
+
// format output into a tri-state outcome consumed by the loud-fail
|
|
6
|
+
// branch in src/index.ts. These tests fence the three outcomes the
|
|
7
|
+
// installer logs name: present / missing / unreachable.
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { classifyOnboardingReadBack } from "../onboarding-readback.js";
|
|
11
|
+
test("non-zero exit → unreachable, with stderr tail captured", () => {
|
|
12
|
+
const outcome = classifyOnboardingReadBack({
|
|
13
|
+
status: 1,
|
|
14
|
+
stdout: "",
|
|
15
|
+
stderr: "Connection refused: bolt://localhost:7687",
|
|
16
|
+
});
|
|
17
|
+
assert.equal(outcome.kind, "unreachable");
|
|
18
|
+
if (outcome.kind === "unreachable") {
|
|
19
|
+
assert.match(outcome.stderrTail, /Connection refused/);
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
test("header row only (no match) → missing", () => {
|
|
23
|
+
// cypher-shell --format plain emits the column header even when zero rows
|
|
24
|
+
// matched. The seed-or-fail contract treats header-only as the loud-fail.
|
|
25
|
+
const outcome = classifyOnboardingReadBack({
|
|
26
|
+
status: 0,
|
|
27
|
+
stdout: "currentStep\n",
|
|
28
|
+
stderr: "",
|
|
29
|
+
});
|
|
30
|
+
assert.equal(outcome.kind, "missing");
|
|
31
|
+
});
|
|
32
|
+
test("data row present → present, currentStep relayed verbatim", () => {
|
|
33
|
+
const outcome = classifyOnboardingReadBack({
|
|
34
|
+
status: 0,
|
|
35
|
+
stdout: "currentStep\n0\n",
|
|
36
|
+
stderr: "",
|
|
37
|
+
});
|
|
38
|
+
assert.equal(outcome.kind, "present");
|
|
39
|
+
if (outcome.kind === "present") {
|
|
40
|
+
assert.equal(outcome.currentStep, "0");
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
test("empty stdout + zero exit → missing (defensive: cypher-shell flushed nothing)", () => {
|
|
44
|
+
const outcome = classifyOnboardingReadBack({
|
|
45
|
+
status: 0,
|
|
46
|
+
stdout: "",
|
|
47
|
+
stderr: "",
|
|
48
|
+
});
|
|
49
|
+
assert.equal(outcome.kind, "missing");
|
|
50
|
+
});
|
|
51
|
+
test("trailing whitespace tolerated on present row", () => {
|
|
52
|
+
const outcome = classifyOnboardingReadBack({
|
|
53
|
+
status: 0,
|
|
54
|
+
stdout: "currentStep\n0 \n\n",
|
|
55
|
+
stderr: "",
|
|
56
|
+
});
|
|
57
|
+
assert.equal(outcome.kind, "present");
|
|
58
|
+
if (outcome.kind === "present") {
|
|
59
|
+
assert.equal(outcome.currentStep, "0");
|
|
60
|
+
}
|
|
61
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, writeFileSync, cpSync, readFileSync, rmSync, rea
|
|
|
4
4
|
import { resolve, join, dirname } from "node:path";
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
6
|
import { resolveInstallPortFromFs, buildMaxyUnitFile, buildClaudeSessionManagerUnitFile } from "./port-resolution.js";
|
|
7
|
+
import { classifyOnboardingReadBack } from "./onboarding-readback.js";
|
|
7
8
|
import { parseOsRelease, isUbuntuLike as isUbuntuLikePure, parseAptCacheCandidate, decideAptResolution, } from "./apt-resolve.js";
|
|
8
9
|
import { findPeerBrandOnDefaultNeo4jPort } from "./peer-brand-detect.js";
|
|
9
10
|
import { requireSupportedPlatform } from "./platform-detect.js";
|
|
@@ -2289,6 +2290,47 @@ function setupAccount() {
|
|
|
2289
2290
|
logFile(` [neo4j] passing NEO4J_URI=${neo4jUri} to seed`);
|
|
2290
2291
|
shell("bash", [seedScript], { cwd: INSTALL_DIR, env: neo4jEnv });
|
|
2291
2292
|
}
|
|
2293
|
+
// Task 033 — install-invariant: OnboardingState must exist post-seed.
|
|
2294
|
+
// Loud-fails (exit 1) on miss because the first admin session has nothing
|
|
2295
|
+
// to read otherwise: loadOnboardingStep returns -1 (no node), the chat
|
|
2296
|
+
// tries to drive the 9-step flow against missing state, and step
|
|
2297
|
+
// advancements have nowhere to land. Symmetric to Task 904's
|
|
2298
|
+
// [install-invariant] line — log-and-continue isn't enough here because
|
|
2299
|
+
// the gap silently breaks every fresh install. Idempotent on re-runs:
|
|
2300
|
+
// the seed's MERGE leaves the node untouched.
|
|
2301
|
+
const accountId = resolveInstallAccountId();
|
|
2302
|
+
if (accountId) {
|
|
2303
|
+
assertOnboardingStateSeeded(accountId, neo4jUri, password);
|
|
2304
|
+
}
|
|
2305
|
+
else {
|
|
2306
|
+
console.log(" [install-invariant] onboarding-state-check SKIPPED reason=no-account-discovered");
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
// Task 033 — Neo4j read-back assertion. Runs cypher-shell against the
|
|
2310
|
+
// brand's Neo4j and confirms an OnboardingState{accountId} node exists.
|
|
2311
|
+
// Loud-fail exit on miss / Neo4j unreachable so the installer logs name
|
|
2312
|
+
// the exact symptom the next run has to fix.
|
|
2313
|
+
function assertOnboardingStateSeeded(accountId, neo4jUri, password) {
|
|
2314
|
+
const TAG = "[install-invariant]";
|
|
2315
|
+
const cypher = `MATCH (o:OnboardingState {accountId: '${accountId}'}) RETURN o.currentStep AS currentStep;`;
|
|
2316
|
+
const result = spawnSync("cypher-shell", ["-u", "neo4j", "-p", password, "-a", neo4jUri, "--format", "plain", cypher], { stdio: "pipe", encoding: "utf-8", timeout: 10_000 });
|
|
2317
|
+
const outcome = classifyOnboardingReadBack({
|
|
2318
|
+
status: result.status,
|
|
2319
|
+
stdout: result.stdout ?? "",
|
|
2320
|
+
stderr: result.stderr ?? "",
|
|
2321
|
+
});
|
|
2322
|
+
if (outcome.kind === "unreachable") {
|
|
2323
|
+
console.error(` ${TAG} onboarding-state-UNREACHABLE accountId=${accountId} exit=${result.status} stderr=${JSON.stringify(outcome.stderrTail)}`);
|
|
2324
|
+
logFile(` ${TAG} onboarding-state-UNREACHABLE accountId=${accountId} stderr=${outcome.stderrTail}`);
|
|
2325
|
+
throw new Error(`OnboardingState read-back failed: cypher-shell exit=${result.status}`);
|
|
2326
|
+
}
|
|
2327
|
+
if (outcome.kind === "missing") {
|
|
2328
|
+
console.error(` ${TAG} onboarding-state-MISSING accountId=${accountId}`);
|
|
2329
|
+
logFile(` ${TAG} onboarding-state-MISSING accountId=${accountId}`);
|
|
2330
|
+
throw new Error(`OnboardingState missing for accountId=${accountId} after seed-neo4j.sh`);
|
|
2331
|
+
}
|
|
2332
|
+
console.log(` ${TAG} onboarding-state-present accountId=${accountId} currentStep=${outcome.currentStep}`);
|
|
2333
|
+
logFile(` ${TAG} onboarding-state-present accountId=${accountId} currentStep=${outcome.currentStep}`);
|
|
2292
2334
|
}
|
|
2293
2335
|
// ---------------------------------------------------------------------------
|
|
2294
2336
|
// Tunnel script shortcuts
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Task 033 — pure parser for the OnboardingState read-back assertion.
|
|
2
|
+
//
|
|
3
|
+
// Extracted into a dedicated module (not src/index.ts) so the test
|
|
4
|
+
// suite can import it without triggering index's brand.json-at-load
|
|
5
|
+
// side effect. The installer's loud-fail branches at
|
|
6
|
+
// `assertOnboardingStateSeeded` consume the tri-state outcome.
|
|
7
|
+
/**
|
|
8
|
+
* Classify cypher-shell --format plain output into one of three states:
|
|
9
|
+
*
|
|
10
|
+
* - `unreachable` — non-zero exit (auth, network, syntax). Stderr tail
|
|
11
|
+
* (last 200 chars) is preserved so the install log carries the
|
|
12
|
+
* symptom verbatim.
|
|
13
|
+
* - `missing` — exit 0 but no data row beyond the header. cypher-shell
|
|
14
|
+
* prints the column name even on a zero-match query.
|
|
15
|
+
* - `present` — exit 0 with at least one data row. `currentStep` is the
|
|
16
|
+
* first field on row 2.
|
|
17
|
+
*/
|
|
18
|
+
export function classifyOnboardingReadBack(input) {
|
|
19
|
+
if (input.status !== 0) {
|
|
20
|
+
return { kind: "unreachable", stderrTail: input.stderr.trim().slice(-200) };
|
|
21
|
+
}
|
|
22
|
+
const lines = input.stdout.trim().split("\n").filter((l) => l.length > 0);
|
|
23
|
+
if (lines.length < 2) {
|
|
24
|
+
return { kind: "missing" };
|
|
25
|
+
}
|
|
26
|
+
return { kind: "present", currentStep: lines[1].trim() };
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rubytech/create-maxy-code",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"description": "Install Maxy — AI for Productive People",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-maxy-code": "./dist/index.js"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"payload"
|
|
18
18
|
],
|
|
19
19
|
"keywords": [
|
|
20
|
-
"maxy",
|
|
20
|
+
"maxy-code",
|
|
21
21
|
"ai",
|
|
22
22
|
"assistant",
|
|
23
23
|
"raspberry-pi",
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"brands": [
|
|
3
3
|
{
|
|
4
|
-
"hostname": "maxy",
|
|
5
|
-
"configDir": ".maxy",
|
|
4
|
+
"hostname": "maxy-code",
|
|
5
|
+
"configDir": ".maxy-code",
|
|
6
6
|
"vncDisplay": 99,
|
|
7
7
|
"rfbPort": 5900,
|
|
8
8
|
"websockifyPort": 6080,
|
|
9
9
|
"cdpPort": 9222
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
|
-
"hostname": "realagent",
|
|
13
|
-
"configDir": ".realagent",
|
|
12
|
+
"hostname": "realagent-code",
|
|
13
|
+
"configDir": ".realagent-code",
|
|
14
14
|
"vncDisplay": 100,
|
|
15
15
|
"rfbPort": 5901,
|
|
16
16
|
"websockifyPort": 6081,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"productName": "Maxy",
|
|
3
|
-
"hostname": "maxy",
|
|
4
|
-
"serviceName": "maxy.service",
|
|
5
|
-
"installDir": "maxy",
|
|
6
|
-
"configDir": ".maxy",
|
|
3
|
+
"hostname": "maxy-code",
|
|
4
|
+
"serviceName": "maxy-code.service",
|
|
5
|
+
"installDir": "maxy-code",
|
|
6
|
+
"configDir": ".maxy-code",
|
|
7
7
|
"tagline": "AI for Productive People",
|
|
8
8
|
"strapline": "Convenience as standard.",
|
|
9
9
|
"domain": "getmaxy.com",
|
|
@@ -28,7 +28,7 @@ set -euo pipefail
|
|
|
28
28
|
|
|
29
29
|
# shellcheck source=_stream-log.sh
|
|
30
30
|
# Resolve symlinks before dirname — ~/reset-tunnel.sh is installed as a symlink
|
|
31
|
-
# (see packages/create-maxy/src/index.ts:installTunnelScripts), so the raw
|
|
31
|
+
# (see packages/create-maxy-code/src/index.ts:installTunnelScripts), so the raw
|
|
32
32
|
# BASH_SOURCE[0] points at $HOME, not the scripts directory where _stream-log.sh lives.
|
|
33
33
|
source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/_stream-log.sh"
|
|
34
34
|
require_stream_log_path reset-tunnel
|
|
@@ -33,7 +33,7 @@ set -euo pipefail
|
|
|
33
33
|
|
|
34
34
|
# shellcheck source=_stream-log.sh
|
|
35
35
|
# Resolve symlinks before dirname — ~/setup-tunnel.sh is installed as a symlink
|
|
36
|
-
# (see packages/create-maxy/src/index.ts:installTunnelScripts), so the raw
|
|
36
|
+
# (see packages/create-maxy-code/src/index.ts:installTunnelScripts), so the raw
|
|
37
37
|
# BASH_SOURCE[0] points at $HOME, not the scripts directory where _stream-log.sh lives.
|
|
38
38
|
source "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/_stream-log.sh"
|
|
39
39
|
require_stream_log_path setup-tunnel
|
|
@@ -39,6 +39,29 @@ This installs all dependencies (Node.js, Neo4j, Cloudflare tunnel, Claude Code),
|
|
|
39
39
|
6. Starts the {{productName}} web server on port 19200
|
|
40
40
|
7. Configures systemd so everything restarts automatically if the Pi reboots
|
|
41
41
|
|
|
42
|
+
## First admin session — onboarding handover
|
|
43
|
+
|
|
44
|
+
After install, the first time you open the admin URL, {{productName}} walks you through a 9-step onboarding flow: plugin selection, output style, thinking-view, timezone, persona, Cloudflare, Anthropic API key, and the business-profile capture. The flow is gated on a single piece of Neo4j state, an `OnboardingState{accountId, currentStep}` node:
|
|
45
|
+
|
|
46
|
+
- `seed-neo4j.sh` writes the node at install time with `currentStep=0`. The installer re-reads it before exiting; a missing node or an unreachable Neo4j fails the install loudly (`[install-invariant] onboarding-state-MISSING` or `onboarding-state-UNREACHABLE`).
|
|
47
|
+
- Every admin session-create reads `currentStep`. Anything below 9 keeps `onboarding_complete=false` on `/api/health` and `/api/admin/session`, and the chat opens on the next step's component (multi-select, dropdown, etc.).
|
|
48
|
+
- The first input on a freshly-spawned admin claude session is prepended with an `<onboarding-state currentStep="N">` directive that tells the agent to call `skill-load skillName=onboarding`. Resumed sessions skip this prepend so the agent doesn't re-ask the same step.
|
|
49
|
+
- If Neo4j is unreachable at session-create, the prepend becomes a loud-fail block (`graphUnreachable="true"`) and `onboarding_complete` is reported false — never silently skipped.
|
|
50
|
+
|
|
51
|
+
Diagnostic command on the Pi (substitute the brand's Neo4j port from `~/.maxy/.env` or `~/.realagent/.env`):
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cypher-shell -a bolt://localhost:<brand-neo4j-port> \
|
|
55
|
+
"MATCH (o:OnboardingState) RETURN o.accountId, o.currentStep"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Failure signals to grep in `~/.maxy/logs/server.log` (or `~/.realagent/logs/server.log`):
|
|
59
|
+
|
|
60
|
+
- missing `[onboarding-seed]` line in install output
|
|
61
|
+
- `[install-invariant] onboarding-state-MISSING`
|
|
62
|
+
- `[onboarding-gate] step=null complete=true` (the pre-Task-033 bug)
|
|
63
|
+
- missing `[skill-load] name=onboarding` while `[onboarding-gate]` reports `complete=false`
|
|
64
|
+
|
|
42
65
|
## Service Management
|
|
43
66
|
|
|
44
67
|
{{productName}} runs via systemd and starts automatically on boot. You don't need to start it manually. To check if it's running, ask {{productName}} "Check system status."
|
|
@@ -121,7 +144,7 @@ A separate operator-side harness at `platform/scripts/installer-device-verify.sh
|
|
|
121
144
|
|
|
122
145
|
The installer registers Claude Code plugins on the device as the last step before the brand service starts. After registration, `claude plugin list` on the Pi shows every Maxy platform plugin shipped by the brand, every premium sub-plugin shipped by the brand, and any external plugins the brand declares (e.g. Telegram, Discord, iMessage from `claude-plugins-official`). Spawned `claude` sessions inherit those plugins from `~/.claude/` — the session manager passes no `--mcp-config` argv.
|
|
123
146
|
|
|
124
|
-
**Where the manifests come from.** The Maxy plugin source tree uses `PLUGIN.md` (YAML frontmatter) for plugin metadata, not Claude Code's native `.claude-plugin/plugin.json`. At bundle time, `scripts/generate-plugin-manifests.mjs` walks the payload and synthesises a Claude-Code-native `plugin.json` per plugin plus a `marketplace.json` at each tree root. The generator runs in `packages/create-maxy/scripts/bundle.js` after platform + premium plugins are copied into the payload, so the deployed install directory carries:
|
|
147
|
+
**Where the manifests come from.** The Maxy plugin source tree uses `PLUGIN.md` (YAML frontmatter) for plugin metadata, not Claude Code's native `.claude-plugin/plugin.json`. At bundle time, `scripts/generate-plugin-manifests.mjs` walks the payload and synthesises a Claude-Code-native `plugin.json` per plugin plus a `marketplace.json` at each tree root. The generator runs in `packages/create-maxy-code/scripts/bundle.js` after platform + premium plugins are copied into the payload, so the deployed install directory carries:
|
|
125
148
|
|
|
126
149
|
- `<INSTALL_DIR>/platform/plugins/<name>/.claude-plugin/plugin.json` per platform plugin
|
|
127
150
|
- `<INSTALL_DIR>/platform/plugins/.claude-plugin/marketplace.json` (marketplace `maxy-platform`)
|
|
@@ -142,7 +165,7 @@ Generator schema:
|
|
|
142
165
|
|
|
143
166
|
Skills, agents, hooks, and commands directories at the plugin root are auto-discovered by Claude Code — no explicit field needed.
|
|
144
167
|
|
|
145
|
-
**Install flow** (`registerLocalAndExternalPlugins()` in `packages/create-maxy/src/index.ts`):
|
|
168
|
+
**Install flow** (`registerLocalAndExternalPlugins()` in `packages/create-maxy-code/src/index.ts`):
|
|
146
169
|
|
|
147
170
|
1. Discover every `.claude-plugin/marketplace.json` under the install directory.
|
|
148
171
|
2. For each one not already in `claude plugin marketplace list`, run `claude plugin marketplace add <dir>`. Pre-existing entries log `[plugin-marketplace] added <name> idempotent=true`.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
# banner — is a non-zero exit with the failing device named on stderr.
|
|
13
13
|
#
|
|
14
14
|
# CONTRACT
|
|
15
|
-
# Archival of any task that touches packages/create-maxy/** is contingent
|
|
15
|
+
# Archival of any task that touches packages/create-maxy-code/** is contingent
|
|
16
16
|
# on this script exiting zero against the published version. The exit code
|
|
17
17
|
# and the per-device summary are quoted in the close commit body.
|
|
18
18
|
#
|
|
@@ -191,7 +191,7 @@ for i in $(seq 0 $((DEVICE_COUNT - 1))); do
|
|
|
191
191
|
# Step 2 — confirm a terminal-success banner in the latest install log.
|
|
192
192
|
# Pick the newest install-*.log by mtime (`ls -t`) and grep for either of
|
|
193
193
|
# the installer's two terminal-success markers emitted by
|
|
194
|
-
# packages/create-maxy/src/index.ts:
|
|
194
|
+
# packages/create-maxy-code/src/index.ts:
|
|
195
195
|
# • DISPLAY_MODE=virtual (Pi, headless VNC) →
|
|
196
196
|
# "Browser automation ready (CDP connected)" (index.ts:3012)
|
|
197
197
|
# • DISPLAY_MODE=native (laptop, on-demand Chromium) →
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# Existing-pi install-log redaction (Task 744).
|
|
3
3
|
#
|
|
4
4
|
# Idempotent one-shot remediation for Pis that completed installation BEFORE
|
|
5
|
-
# the install-log redaction landed at packages/create-maxy/src/index.ts:152.
|
|
5
|
+
# the install-log redaction landed at packages/create-maxy-code/src/index.ts:152.
|
|
6
6
|
# Scans every `install-*.log` in the configured logs directory and replaces
|
|
7
7
|
# every literal `set-initial-password ...<secret>` payload with
|
|
8
8
|
# `set-initial-password [REDACTED]`. Re-running the script is safe —
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# edits occur.
|
|
11
11
|
#
|
|
12
12
|
# Source patterns covered:
|
|
13
|
-
# 1. TS installer (packages/create-maxy/src/index.ts:152) — "[ISO] > sudo
|
|
13
|
+
# 1. TS installer (packages/create-maxy-code/src/index.ts:152) — "[ISO] > sudo
|
|
14
14
|
# neo4j-admin dbms set-initial-password -- <secret>" or any args after
|
|
15
15
|
# "set-initial-password" (positional or "--" delimited).
|
|
16
16
|
# 2. Legacy bash installer (removed) — "+ sudo neo4j-admin dbms
|
|
@@ -541,4 +541,39 @@ BACKFILL_EOF
|
|
|
541
541
|
echo " [backfill] $BACKFILL_RESULT"
|
|
542
542
|
fi
|
|
543
543
|
|
|
544
|
+
# ------------------------------------------------------------------
|
|
545
|
+
# 4. Seed OnboardingState (Task 033 — maxy-code)
|
|
546
|
+
# ------------------------------------------------------------------
|
|
547
|
+
# The first admin session reads OnboardingState.currentStep to decide whether
|
|
548
|
+
# the chat opens on step 1 of the 9-step onboarding flow or on a free-form
|
|
549
|
+
# greeting. Pre-Task-033 the node was never written at seed, so the post-PIN
|
|
550
|
+
# session-create's loadOnboardingStep returned -1 (no node) which used to
|
|
551
|
+
# conflate with null (Neo4j unreachable) and silently report
|
|
552
|
+
# onboardingComplete:true — the operator was dropped into the admin agent
|
|
553
|
+
# with no plugin selection, output-style, timezone, SOUL, Cloudflare,
|
|
554
|
+
# Anthropic-key, or business-profile setup.
|
|
555
|
+
#
|
|
556
|
+
# Keyed on accountId, not userId — fresh installs reach the seed before
|
|
557
|
+
# users.json exists (PIN is set later by the onboarding flow itself).
|
|
558
|
+
# MERGE is idempotent: re-runs leave currentStep untouched, refreshing only
|
|
559
|
+
# updatedAt.
|
|
560
|
+
SEED_NOW=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
561
|
+
echo "==> Seeding OnboardingState for accountId=$ACCOUNT_ID"
|
|
562
|
+
ONBOARDING_SEED_RESULT=$("$CYPHER_SHELL" -u "$NEO4J_USER" -p "$NEO4J_PASSWORD" -a "$NEO4J_URI" --format plain << CYPHER_EOF
|
|
563
|
+
MERGE (o:OnboardingState {accountId: '$ACCOUNT_ID'})
|
|
564
|
+
ON CREATE SET o.currentStep = 0,
|
|
565
|
+
o.createdAt = '$SEED_NOW',
|
|
566
|
+
o.updatedAt = '$SEED_NOW'
|
|
567
|
+
ON MATCH SET o.updatedAt = '$SEED_NOW'
|
|
568
|
+
RETURN o.currentStep AS currentStep, o.createdAt = '$SEED_NOW' AS created;
|
|
569
|
+
CYPHER_EOF
|
|
570
|
+
)
|
|
571
|
+
ONBOARDING_STEP=$(echo "$ONBOARDING_SEED_RESULT" | awk 'NR==2 {print $1}')
|
|
572
|
+
ONBOARDING_CREATED=$(echo "$ONBOARDING_SEED_RESULT" | awk 'NR==2 {print $2}')
|
|
573
|
+
if [ "$ONBOARDING_CREATED" = "true" ]; then
|
|
574
|
+
echo " [onboarding-seed] accountId=$ACCOUNT_ID currentStep=$ONBOARDING_STEP idempotent=false"
|
|
575
|
+
else
|
|
576
|
+
echo " [onboarding-seed] accountId=$ACCOUNT_ID currentStep=$ONBOARDING_STEP idempotent=true"
|
|
577
|
+
fi
|
|
578
|
+
|
|
544
579
|
echo " Done."
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
# and where a skill prescribes a forbidden direct-execution path
|
|
9
9
|
# (`cypher-shell`, `neo4j-admin` invocations, raw-Cypher DML in prose).
|
|
10
10
|
#
|
|
11
|
-
# Wired into the root `packages/create-maxy/package.json` `prepublishOnly`
|
|
11
|
+
# Wired into the root `packages/create-maxy-code/package.json` `prepublishOnly`
|
|
12
12
|
# script so a regression cannot reach npm publish without firing.
|
|
13
13
|
#
|
|
14
14
|
# One stdout line per (skill, specialist) pair:
|
|
@@ -45,7 +45,7 @@ BRAND_JSON="${PLATFORM_ROOT}/config/brand.json"
|
|
|
45
45
|
# Task 959 — brand.json is the single source of truth at runtime; missing
|
|
46
46
|
# fields loud-fail rather than silently substituting a vncDisplay-derived
|
|
47
47
|
# offset (silent-fallback-masks-root-cause recurrence). The install-time
|
|
48
|
-
# offset rule in packages/create-maxy/src/index.ts stamps every brand at
|
|
48
|
+
# offset rule in packages/create-maxy-code/src/index.ts stamps every brand at
|
|
49
49
|
# build time, so a correctly-installed device always has all five fields.
|
|
50
50
|
if [ ! -f "$BRAND_JSON" ]; then
|
|
51
51
|
echo "[vnc.sh] error reason=brand-config-missing path=$BRAND_JSON" >&2
|
|
@@ -89,7 +89,7 @@ mkdir -p "$LOG_DIR"
|
|
|
89
89
|
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; }
|
|
90
90
|
|
|
91
91
|
# Task 929 — resolve the absolute Chromium binary path from the install-time
|
|
92
|
-
# config file. The installer (packages/create-maxy/src/index.ts
|
|
92
|
+
# config file. The installer (packages/create-maxy-code/src/index.ts
|
|
93
93
|
# ensureNonSnapChromium + writeChromiumBinaryPathFile) writes this file with
|
|
94
94
|
# the non-snap binary chosen for this device — `/usr/bin/chromium` on Pi
|
|
95
95
|
# Bookworm (real .deb) or `/usr/bin/google-chrome-stable` on Ubuntu Noble
|
|
@@ -80,7 +80,7 @@ Two PNGs of the wordmark/lockup as the brand uses it. **The filename describes t
|
|
|
80
80
|
| Filename | Art colour | Place it on |
|
|
81
81
|
|---|---|---|
|
|
82
82
|
| `<slug>-logo-light.png` | light/white/cream/pale pixels | dark surfaces (back-page hero, dark masthead) |
|
|
83
|
-
| `<slug>-logo-dark.png` | dark/black/navy/charcoal pixels | light surfaces (TOC,
|
|
83
|
+
| `<slug>-logo-dark.png` | dark/black/navy/charcoal pixels | light surfaces (TOC, index.html nav, paper backgrounds) |
|
|
84
84
|
|
|
85
85
|
So if you open `donnavincent-logo-light.png` and see white text, that is correct — it's the light-coloured variant. If you open `donnavincent-logo-dark.png` and see white text, the file was misnamed by an earlier run; rename it.
|
|
86
86
|
|
|
@@ -23,7 +23,7 @@ The deliverable is identical to `property-brochure`'s outcome — written into `
|
|
|
23
23
|
|
|
24
24
|
- `brochure.html` plus per-page 300 dpi PNG print snapshots (canonical, archival)
|
|
25
25
|
- Two image-only PDFs: `<slug>-brochure-print.pdf` (300 dpi print master) and `<slug>-brochure-web.pdf` (192 dpi web/digital). Both built from per-page Playwright snapshots via img2pdf and linearized via qpdf — no Ghostscript anywhere in the chain. See `property-brochure → PDF deliverable` and `a4-print-documents → PDF deliverables`.
|
|
26
|
-
- A self-contained **web bundle** at `output/web/` plus `output/<slug>-web.zip` — the smaller, ready-to-host version for property micro-sites; includes both the brochure and a companion **landing page** at `
|
|
26
|
+
- A self-contained **web bundle** at `output/web/` plus `output/<slug>-web.zip` — the smaller, ready-to-host version for property micro-sites; includes both the brochure and a companion **landing page** at `index.html` that presents the property as a continuous scrollable web page. See `property-brochure → Web bundle` and `property-brochure → Page`
|
|
27
27
|
|
|
28
28
|
The orchestrator's value is in what it does **not** redo:
|
|
29
29
|
|
|
@@ -271,19 +271,19 @@ After a complete run, the on-disk layout is **exactly** this — no deeper nesti
|
|
|
271
271
|
qr-video.png, qr-listing.png
|
|
272
272
|
web/ # self-contained web bundle, generated alongside print archive
|
|
273
273
|
brochure.html # same HTML; print-img refs point at .jpg snapshots; QR codes are clickable <a>
|
|
274
|
-
|
|
274
|
+
index.html # companion property landing page (Modern House-style scroll)
|
|
275
275
|
<property_slug>-brochure.pdf # digital PDF, copied for the in-page Download button
|
|
276
276
|
cover-print.jpg
|
|
277
277
|
page2-print.jpg … page15-print.jpg
|
|
278
278
|
backpage-print.jpg
|
|
279
279
|
images/ # web-tier per-slot encodings: hero 1300/q82, story 1100/q80, thumb 800/q76
|
|
280
280
|
<property_slug>-NN.webp
|
|
281
|
-
<property_slug>-hero-1-main.webp #
|
|
282
|
-
<property_slug>-hero-2-<role>.webp #
|
|
283
|
-
<property_slug>-hero-3-<role>.webp #
|
|
281
|
+
<property_slug>-hero-1-main.webp # index.html hero rotator — source-res @ q88
|
|
282
|
+
<property_slug>-hero-2-<role>.webp # index.html hero rotator — interior moment
|
|
283
|
+
<property_slug>-hero-3-<role>.webp # index.html hero rotator — garden / outdoor
|
|
284
284
|
<property_slug>-floorplan.png # line-art PNG, copied unchanged from canonical
|
|
285
|
-
<brand_slug>-logo-light.png # light-coloured logo art — for dark surfaces (e.g. dark
|
|
286
|
-
<brand_slug>-logo-dark.png # dark-coloured logo art — for light surfaces (e.g.
|
|
285
|
+
<brand_slug>-logo-light.png # light-coloured logo art — for dark surfaces (e.g. dark index.html sections)
|
|
286
|
+
<brand_slug>-logo-dark.png # dark-coloured logo art — for light surfaces (e.g. index.html nav, brochure cover when paper)
|
|
287
287
|
qr-video.png, qr-listing.png
|
|
288
288
|
<property_slug>-web.zip # zipped form of web/, ready to upload to a static-site host
|
|
289
289
|
```
|
|
@@ -325,7 +325,7 @@ Determinism rules — a run that violates any of these is wrong, even if the bro
|
|
|
325
325
|
| "House" instead of "home" at super-premium register | Wrong word for the buyer. "Home" is warmer, more aspirational, and matches the register. Replace globally. |
|
|
326
326
|
| Text overlaid on photographs relying on a gradient + text-shadow for legibility | Insufficient against busy backgrounds (stone tile, golden-hour skies). Use a translucent dark panel directly behind the text. See `property-brochure → Text on images`. |
|
|
327
327
|
| Including a detached gymnasium-summerhouse footprint in the principal home's first-floor sq ft | Misrepresents £/sq ft. List ancillary footprints separately. See `property-brochure → Floor area`. |
|
|
328
|
-
| Shipping a web bundle without `
|
|
328
|
+
| Shipping a web bundle without `index.html` | The web bundle is the property's hosted experience. The brochure is a downloadable folio; the landing page is the scrollable "for sale" page that links to it. Both are mandatory in the zip. See `property-brochure → Page`. |
|
|
329
329
|
| Putting QR codes on the back page without a visible URL or a clickable wrapper | A QR is unscannable from the digital screen showing the brochure. The clickable URL beneath each QR is mandatory; it makes the digital reader experience equivalent to the print reader experience. See `property-brochure → Back-page QR codes`. |
|
|
330
330
|
| Using the same logo variant for the back-page masthead (dark surface) and the landing-page nav (light surface) | The two surfaces need opposite logo variants — white-on-transparent for the back page, dark/coloured for the landing nav. Both must be present in `output/web/images/`. |
|
|
331
331
|
|
|
@@ -339,7 +339,7 @@ Report `DONE` only after:
|
|
|
339
339
|
- Both PDF deliverables exist alongside `brochure.html`: the print master (`<property_slug>-brochure-print.pdf`, ~50–80 MB at 300 dpi) and the web/digital (`<property_slug>-brochure-web.pdf`, ~20–35 MB at 192 dpi). Both are image-only, built from per-page Playwright snapshots via img2pdf and linearized via qpdf; both pass `qpdf --check`, both report `Optimized: yes`, and both have zero embedded fonts (`pdffonts | wc -l` returns 2). See `a4-print-documents → Verification`.
|
|
340
340
|
- All per-page print snapshots referenced by the HTML's print CSS exist at expected names — `cover-print.png`, `page2-print.png` through `page15-print.png`, and `backpage-print.png` (16 files for the 16-page folio) — at the orientation-correct 300 dpi pixel dimensions (3509×2481 landscape / 2481×3509 portrait).
|
|
341
341
|
- The brochure has an **even page count** (canonical 16; multiples of 2 required, ideally multiples of 4 for booklet binding) — see `a4-print-documents` → Even-page count for duplex printing.
|
|
342
|
-
- The **web bundle** exists at `output/web/` and as a zipped `output/<slug>-web.zip` — see `property-brochure → Web bundle` for what it contains. The bundle includes both `brochure.html` and the companion `
|
|
342
|
+
- The **web bundle** exists at `output/web/` and as a zipped `output/<slug>-web.zip` — see `property-brochure → Web bundle` for what it contains. The bundle includes both `brochure.html` and the companion `index.html` (see `property-brochure → Page`), plus a copy of the web PDF named simply `<property_slug>-brochure.pdf` to match the in-page Download links. Smoke-test was performed: an isolated HTTP server returned 200 for every referenced asset (HTML, web-tier images, jpg snapshots, the bundled PDF), and **both** `brochure.html` and `index.html` rendered correctly. Typical zip is 30–50 MB for a 16-page folio.
|
|
343
343
|
- **Back-page QR codes are clickable.** Each QR is wrapped in an `<a target="_blank">` tag and accompanied by a visible underlined URL beneath the label, so a digital reader can click instead of scan. See `property-brochure → Back-page QR codes`.
|
|
344
344
|
- The user-facing report names which steps ran and which were reused, **and the chosen orientation**, **and the path to both the canonical print archive and the web zip**.
|
|
345
345
|
- No image was read in violation of the 2000px rule (verifiable: every image read should have been preceded by a `sips`/`identify` measurement, and any over-threshold image was previewed via `/tmp/`).
|
package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ Produce an A4 property brochure from raw assets. The default deliverable is a 16
|
|
|
9
9
|
|
|
10
10
|
## Output location
|
|
11
11
|
|
|
12
|
-
By default the brochure is written to the **caller's current working directory**. The skill creates `./output/brochure.html`, `./output/web/
|
|
12
|
+
By default the brochure is written to the **caller's current working directory**. The skill creates `./output/brochure.html`, `./output/web/index.html`, the print PNGs, and the two PDFs underneath `pwd` — no inference, no nesting under a brand workspace.
|
|
13
13
|
|
|
14
14
|
If you want the output elsewhere, pass `output_dir` explicitly (an absolute path or a relative path resolved against CWD). Common overrides:
|
|
15
15
|
|
|
@@ -23,12 +23,12 @@ Do not silently nest the output under a brand workspace just because one is pres
|
|
|
23
23
|
|
|
24
24
|
## Step 1 — copy the canonical templates (do this first)
|
|
25
25
|
|
|
26
|
-
`references/template.html` and `references/
|
|
26
|
+
`references/template.html` and `references/index.html` are the **only** acceptable starting points. They are placeholder-only — every property value is a `{{ token }}`. Copy them before doing anything else. **Do not write brochure HTML or page HTML from scratch. Do not start from a sibling property's output. Do not improvise structure from prose.**
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
29
|
mkdir -p <output_dir>/web
|
|
30
30
|
cp ~/.claude/plugins/real-estate-brochure/skills/property-brochure/references/template.html <output_dir>/brochure.html
|
|
31
|
-
cp ~/.claude/plugins/real-estate-brochure/skills/property-brochure/references/
|
|
31
|
+
cp ~/.claude/plugins/real-estate-brochure/skills/property-brochure/references/index.html <output_dir>/web/index.html
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
`<output_dir>` is whatever the **Output location** section above resolves to — typically `./output/` in the caller's CWD.
|
|
@@ -43,7 +43,7 @@ Both files must contain canonical sentinels (proves they came from `references/`
|
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
grep -q '{{ property_name }}' <output_dir>/brochure.html || echo "FAIL: brochure not from template"
|
|
46
|
-
grep -q 'hero-stage' <output_dir>/web/
|
|
46
|
+
grep -q 'hero-stage' <output_dir>/web/index.html || echo "FAIL: index.html not from references"
|
|
47
47
|
```
|
|
48
48
|
|
|
49
49
|
Both greps must succeed before populating content. If either fails, recopy.
|
|
@@ -74,14 +74,14 @@ Read the seller brief once before writing any copy. Then walk the `{{ placeholde
|
|
|
74
74
|
| Editorial copy register, em-dash policy, "home" vs "house", text-on-images, typography, stats rows, AI hero prompt, **per-section composition guide** | [references/copy.md](references/copy.md) |
|
|
75
75
|
| Orientation, 16-page layout, cover, floorplan, distinguishing features, Material Information, QR codes, location map | [references/structure.md](references/structure.md) |
|
|
76
76
|
| Live editing, snapshot capture, PDF deliverable, web bundle | [references/build.md](references/build.md) |
|
|
77
|
-
| `
|
|
77
|
+
| `index.html` landing page — sections, hero rotator, mobile/drawer | [references/index-landing.md](references/index-landing.md) |
|
|
78
78
|
|
|
79
79
|
### Substitution completion gate
|
|
80
80
|
|
|
81
81
|
A brochure with any `{{ x }}` still in the rendered HTML is **shipped broken**. Before any snapshot capture or PDF build, run:
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
|
-
unsubstituted=$(grep -c '{{' "$output_dir/brochure.html" "$output_dir/web/
|
|
84
|
+
unsubstituted=$(grep -c '{{' "$output_dir/brochure.html" "$output_dir/web/index.html" 2>/dev/null | awk -F: '{s+=$2} END {print s}')
|
|
85
85
|
[ "$unsubstituted" -eq 0 ] || { echo "DEFECT: $unsubstituted unsubstituted {{ tokens }} remain"; exit 1; }
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -106,7 +106,7 @@ unsubstituted=$(grep -c '{{' "$output_dir/brochure.html" "$output_dir/web/page.h
|
|
|
106
106
|
- **Page count must be even** (16 default; 12 if the property is too small). 14 is forbidden.
|
|
107
107
|
- **Image-only PDFs** — no fonts embedded, no Ghostscript anywhere in the chain.
|
|
108
108
|
- **Self-contained `output/`** — never reference `../` paths. Every asset the HTML uses must live under `output/images/`.
|
|
109
|
-
- **Web bundle must include `
|
|
109
|
+
- **Web bundle must include `index.html`** — a bundle without it is incomplete.
|
|
110
110
|
- **Suppress screen-only chrome before snapshotting** — the template's `.download-bar` is hidden under `@media print` but Playwright's `element.screenshot()` captures the screen render, where the bar IS visible. The render script in `build.md` injects a style tag (`.download-bar { display: none !important; }`) before capture. Any custom render script must do the same.
|
|
111
111
|
- **No `{{ token }}` may remain in rendered output** — `grep '{{' output/brochure.html` must return zero matches before PDFs are built.
|
|
112
112
|
|
|
@@ -164,7 +164,7 @@ The web bundle is a parallel directory with the same on-disk shape as `output/`
|
|
|
164
164
|
```
|
|
165
165
|
output/web/
|
|
166
166
|
brochure.html # identical content; only print-img references switched .png → .jpg
|
|
167
|
-
|
|
167
|
+
index.html # companion landing page (mandatory in bundle)
|
|
168
168
|
cover-print.jpg, page2-print.jpg … backpage-print.jpg # 96 dpi JPEG snapshots (q=88)
|
|
169
169
|
<slug>-brochure.pdf # identical bytes to <slug>-brochure-web.pdf at the property level — simpler name inside the bundle since there's only one PDF here
|
|
170
170
|
images/
|
|
@@ -220,7 +220,7 @@ A 16-page folio's web-bundle snapshots total ~3 MB (vs ~85 MB for the canonical
|
|
|
220
220
|
|
|
221
221
|
### Copy the web PDF into the bundle
|
|
222
222
|
|
|
223
|
-
The web PDF (`<slug>-brochure-web.pdf` at the property level) is also placed inside the bundle, named simply `<slug>-brochure.pdf` to match `
|
|
223
|
+
The web PDF (`<slug>-brochure-web.pdf` at the property level) is also placed inside the bundle, named simply `<slug>-brochure.pdf` to match `index.html` and `brochure.html` link conventions (where there's only one PDF in scope, no `-web` suffix is needed):
|
|
224
224
|
|
|
225
225
|
```bash
|
|
226
226
|
cp output/<slug>-brochure-web.pdf output/web/<slug>-brochure.pdf
|
|
@@ -245,7 +245,7 @@ Serve the unzipped bundle from an isolated temp directory and verify every refer
|
|
|
245
245
|
TMP=/tmp/web-test && rm -rf $TMP && mkdir -p $TMP
|
|
246
246
|
cd $TMP && unzip -q /path/to/<slug>-web.zip
|
|
247
247
|
python3 -m http.server 8765 &
|
|
248
|
-
for f in brochure.html
|
|
248
|
+
for f in brochure.html index.html cover-print.jpg images/<slug>-01.webp images/<brand>-logo-light.png <slug>-brochure.pdf; do
|
|
249
249
|
echo "$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8765/$f) $f"
|
|
250
250
|
done
|
|
251
251
|
```
|
|
@@ -257,10 +257,10 @@ The brochure should render the same as the canonical preview, just lighter on th
|
|
|
257
257
|
| File | `output/` (canonical archive) | `output/web/` (web bundle) |
|
|
258
258
|
|---|---|---|
|
|
259
259
|
| `brochure.html` | full-resolution images, `.png` snapshot refs | identical structure, `.jpg` snapshot refs, smaller image refs |
|
|
260
|
-
| `
|
|
260
|
+
| `index.html` | — | ✓ companion landing page; see `index-landing.md` |
|
|
261
261
|
| `<slug>-brochure-print.pdf` | ✓ canonical print master (50–80 MB, 300 dpi) | — (too large to bundle) |
|
|
262
262
|
| `<slug>-brochure-web.pdf` | ✓ web/digital deliverable (20–35 MB, 192 dpi) | — copied into bundle as `<slug>-brochure.pdf` |
|
|
263
|
-
| `<slug>-brochure.pdf` | — | ✓ identical bytes to `-web.pdf`; matches
|
|
263
|
+
| `<slug>-brochure.pdf` | — | ✓ identical bytes to `-web.pdf`; matches index.html / brochure.html link |
|
|
264
264
|
| `cover-print.png … backpage-print.png` | ✓ 300 dpi PNG (~4–7 MB each) — canonical snapshots | — replaced by 96 dpi `.jpg` versions |
|
|
265
265
|
| `cover-print.jpg … backpage-print.jpg` | — | ✓ 96 dpi JPEG (~150 KB each, derived from the 300 dpi PNGs) |
|
|
266
266
|
| `images/<slug>-NN.webp` | full-quality per Render-slot table | web-tier per the web table above |
|
|
@@ -109,7 +109,7 @@ If five cells at the chosen font size do not fit the available row width, **shri
|
|
|
109
109
|
|
|
110
110
|
## Per-section composition guide
|
|
111
111
|
|
|
112
|
-
This section is the writing brief for each placeholder block in `template.html` and `
|
|
112
|
+
This section is the writing brief for each placeholder block in `template.html` and `index.html`. The token names map 1:1 to entries in `placeholders.md`; this file says **how the copy should read** for each one.
|
|
113
113
|
|
|
114
114
|
### Cover & subtitle (page 1)
|
|
115
115
|
|
|
@@ -195,9 +195,9 @@ This section is the writing brief for each placeholder block in `template.html`
|
|
|
195
195
|
- `{{ backpage_headline }}` — canonical phrasing "Arrange a viewing of <em>{{ property_name }}</em>". Do not paraphrase.
|
|
196
196
|
- `{{ backpage_tagline }}` — single sentence, 25-45 words. Mirrors the cover/opener without duplicating phrases. Close with "Best understood in person." or an equivalent property-specific clincher.
|
|
197
197
|
|
|
198
|
-
### Landing page (
|
|
198
|
+
### Landing page (index.html)
|
|
199
199
|
|
|
200
|
-
The landing-page tokens (`{{ landing_* }}`) source the same brief but render in continuous-scroll form rather than a 16-page folio. Each `{{ landing_*_para_N }}` follows the same length/voice rules as the equivalent brochure paragraph — see placeholders.md → "Category 9 —
|
|
200
|
+
The landing-page tokens (`{{ landing_* }}`) source the same brief but render in continuous-scroll form rather than a 16-page folio. Each `{{ landing_*_para_N }}` follows the same length/voice rules as the equivalent brochure paragraph — see placeholders.md → "Category 9 — index.html editorial copy" for the per-section breakdown.
|
|
201
201
|
|
|
202
202
|
## AI hero image prompt
|
|
203
203
|
|
|
@@ -120,7 +120,7 @@ The brochure is a **print-first deliverable**. Source images target the print-ma
|
|
|
120
120
|
| **Map / screenshot** | screenshot with text labels | **2000 px** | source size, no resize | 88 |
|
|
121
121
|
| **EPC chart** | small, already <100KB — copy through unchanged | n/a | as-is | as-is |
|
|
122
122
|
|
|
123
|
-
**Floor plan exception — keep the source file as-is.** When the seller (or the agency's floor-plan provider) supplies a high-resolution PNG, **use it directly** — do not convert to WebP, do not downscale, do not re-encode. Floor plans are line art with thin strokes and crisp text where any quality loss is visibly worse than the same quality loss on photographic content. The same file (`<slug>-floorplan.png`) is referenced in the canonical brochure HTML, in the
|
|
123
|
+
**Floor plan exception — keep the source file as-is.** When the seller (or the agency's floor-plan provider) supplies a high-resolution PNG, **use it directly** — do not convert to WebP, do not downscale, do not re-encode. Floor plans are line art with thin strokes and crisp text where any quality loss is visibly worse than the same quality loss on photographic content. The same file (`<slug>-floorplan.png`) is referenced in the canonical brochure HTML, in the index.html landing page, and copied verbatim into `output/web/images/`. Because the new PDF pipeline rasterises whole pages from the rendered HTML (rather than passing the floor plan through a separate compression stage), the floor plan's quality at the PDF stage is determined by the snapshot DPI — 300 dpi for the print master, 192 dpi for the web PDF. A typical 2000 px floor plan slotted into a ~150 mm-wide brochure cell renders crisply at 300 dpi without any per-image processing. The **web bundle** also references the same `.png` (not a `.webp` substitute) — line art doesn't compress well as WebP and the file size is already modest.
|
|
124
124
|
|
|
125
125
|
The **source-px floor** is the floor on the input photo's longest edge **before optimisation** — see the `Image resolution floor` section in the **a4-print-documents** SKILL for the rationale and full thresholds.
|
|
126
126
|
|