@rubytech/create-maxy-code 0.1.111 → 0.1.113
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__/brew-install.test.js +10 -0
- package/dist/__tests__/brew-resolve.test.js +35 -0
- package/dist/__tests__/launchd-plist.test.js +46 -0
- package/dist/__tests__/macos-darwin-branch.test.js +85 -0
- package/dist/brew-install.js +5 -0
- package/dist/index.js +26 -6
- package/package.json +1 -1
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +105 -0
- package/payload/platform/plugins/docs/references/deployment.md +22 -4
- package/payload/platform/plugins/memory/mcp/dist/index.js +35 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.d.ts +15 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.js +64 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/image-fetch.js.map +1 -0
- package/payload/platform/plugins/memory/references/schema-estate-agent.md +76 -1
- package/payload/platform/plugins/venture-studio/PLUGIN.md +13 -4
- package/payload/platform/plugins/venture-studio/bin/scaffold.sh +15 -3
- package/payload/platform/plugins/venture-studio/skills/brand-pack/SKILL.md +1 -1
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/SKILL.md +27 -14
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/business-plan-template.md +7 -4
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/data-room-structure.md +16 -5
- package/payload/platform/plugins/venture-studio/skills/investor-data-room/references/deck-blueprint-template.md +7 -6
- package/payload/platform/plugins/venture-studio/skills/office-hours/SKILL.md +8 -9
- package/payload/platform/plugins/venture-studio/skills/prototype-host/SKILL.md +179 -0
- package/payload/platform/plugins/venture-studio/skills/prototype-host/references/cloudflared-ingress-edit.md +81 -0
- package/payload/platform/plugins/venture-studio/skills/prototype-host/references/scaffold-frameworks.md +60 -0
- package/payload/platform/plugins/venture-studio/skills/prototype-host/references/systemd-user-service.md +104 -0
- package/payload/platform/plugins/venture-studio/skills/zero-to-prototype/SKILL.md +4 -0
- package/payload/platform/templates/agents/public/IDENTITY.md +11 -1
- package/payload/premium-plugins/real-agent/BUNDLE.md +3 -2
- package/payload/premium-plugins/real-agent/agents/listing-curator.md +152 -0
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/a4-print-documents/SKILL.md +1 -1
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/brand-design/SKILL.md +5 -11
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/make-brochure/SKILL.md +7 -7
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-extract/SKILL.md +31 -39
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-market-report/SKILL.md +4 -4
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-socials/SKILL.md +3 -2
- package/payload/premium-plugins/real-agent/plugins/buyers/.claude-plugin/plugin.json +1 -1
- package/payload/premium-plugins/real-agent/plugins/buyers/PLUGIN.md +3 -2
- package/payload/premium-plugins/real-agent/plugins/buyers/skills/property-recommender/SKILL.md +96 -0
- package/payload/premium-plugins/venture-studio/PLUGIN.md +13 -4
- package/payload/premium-plugins/venture-studio/bin/scaffold.sh +15 -3
- package/payload/premium-plugins/venture-studio/skills/brand-pack/SKILL.md +1 -1
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/SKILL.md +27 -14
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/business-plan-template.md +7 -4
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/data-room-structure.md +16 -5
- package/payload/premium-plugins/venture-studio/skills/investor-data-room/references/deck-blueprint-template.md +7 -6
- package/payload/premium-plugins/venture-studio/skills/office-hours/SKILL.md +8 -9
- package/payload/premium-plugins/venture-studio/skills/prototype-host/SKILL.md +179 -0
- package/payload/premium-plugins/venture-studio/skills/prototype-host/references/cloudflared-ingress-edit.md +81 -0
- package/payload/premium-plugins/venture-studio/skills/prototype-host/references/scaffold-frameworks.md +60 -0
- package/payload/premium-plugins/venture-studio/skills/prototype-host/references/systemd-user-service.md +104 -0
- package/payload/premium-plugins/venture-studio/skills/zero-to-prototype/SKILL.md +4 -0
|
@@ -73,9 +73,14 @@ test("decideBrewInstallPlan: all pkgs already installed → empty install list,
|
|
|
73
73
|
],
|
|
74
74
|
});
|
|
75
75
|
assert.deepEqual(plan.toInstall, []);
|
|
76
|
+
// Each pkg emits the legacy `[homebrew]` line plus the uniform-prefix
|
|
77
|
+
// `[create-maxy] brew-formula=… action=…` mirror Task 297 added for the
|
|
78
|
+
// grep recipe.
|
|
76
79
|
assert.deepEqual(plan.logs, [
|
|
77
80
|
" [homebrew] node@22 already installed (v22.18.1)",
|
|
81
|
+
" [create-maxy] brew-formula=node@22 action=present",
|
|
78
82
|
" [homebrew] neo4j already installed (v5.20.0)",
|
|
83
|
+
" [create-maxy] brew-formula=neo4j action=present",
|
|
79
84
|
]);
|
|
80
85
|
});
|
|
81
86
|
test("decideBrewInstallPlan: some missing → install list contains only missing formulas, install + already logs interleaved", () => {
|
|
@@ -89,8 +94,11 @@ test("decideBrewInstallPlan: some missing → install list contains only missing
|
|
|
89
94
|
assert.deepEqual(plan.toInstall, ["neo4j", "cloudflared"]);
|
|
90
95
|
assert.deepEqual(plan.logs, [
|
|
91
96
|
" [homebrew] node@22 already installed (v22.18.1)",
|
|
97
|
+
" [create-maxy] brew-formula=node@22 action=present",
|
|
92
98
|
" [homebrew] installing neo4j …",
|
|
99
|
+
" [create-maxy] brew-formula=neo4j action=install",
|
|
93
100
|
" [homebrew] installing cloudflared …",
|
|
101
|
+
" [create-maxy] brew-formula=cloudflared action=install",
|
|
94
102
|
]);
|
|
95
103
|
});
|
|
96
104
|
test("decideBrewInstallPlan: all missing → install list = every formula, only install logs", () => {
|
|
@@ -103,7 +111,9 @@ test("decideBrewInstallPlan: all missing → install list = every formula, only
|
|
|
103
111
|
assert.deepEqual(plan.toInstall, ["node@22", "neo4j"]);
|
|
104
112
|
assert.deepEqual(plan.logs, [
|
|
105
113
|
" [homebrew] installing node@22 …",
|
|
114
|
+
" [create-maxy] brew-formula=node@22 action=install",
|
|
106
115
|
" [homebrew] installing neo4j …",
|
|
116
|
+
" [create-maxy] brew-formula=neo4j action=install",
|
|
107
117
|
]);
|
|
108
118
|
});
|
|
109
119
|
// ---------------------------------------------------------------------------
|
|
@@ -101,3 +101,38 @@ test("DARWIN_ALIASES contains nodejs → node@22 and is the only entry", () => {
|
|
|
101
101
|
assert.equal(DARWIN_ALIASES.nodejs, "node@22");
|
|
102
102
|
assert.equal(Object.keys(DARWIN_ALIASES).length, 1);
|
|
103
103
|
});
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// DARWIN_BASE_DEPS + neo4j shape lock — every brew dep the darwin install
|
|
106
|
+
// branch feeds into installAllBrewPackages must either (a) be a real Homebrew
|
|
107
|
+
// core formula already, or (b) resolve to one via DARWIN_ALIASES. The list is
|
|
108
|
+
// duplicated here from index.ts:installSystemDeps so a future edit of either
|
|
109
|
+
// surface has to keep them in sync.
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Mirrors `DARWIN_BASE_DEPS` in packages/create-maxy-code/src/index.ts. Every
|
|
112
|
+
// entry must be either a real brew formula (pass-through) or an alias.
|
|
113
|
+
const DARWIN_BASE_DEPS_FIXTURE = ["curl", "git", "unzip", "jq", "poppler", "ffmpeg"];
|
|
114
|
+
// `neo4j` and `cloudflared` are passed to installAllBrewPackages from
|
|
115
|
+
// installNeo4j (index.ts:1265) and installCloudflared (index.ts:1728). Both are
|
|
116
|
+
// concrete brew formula names — no alias hop expected.
|
|
117
|
+
const DARWIN_SOLO_DEPS_FIXTURE = ["neo4j", "cloudflared"];
|
|
118
|
+
test("every DARWIN_BASE_DEPS entry passes through the resolver as the brew formula name", () => {
|
|
119
|
+
for (const pkg of DARWIN_BASE_DEPS_FIXTURE) {
|
|
120
|
+
const r = decideBrewResolution({ pkg, brewInstalled: false });
|
|
121
|
+
assert.equal(r.resolved, pkg, `DARWIN_BASE_DEPS entry '${pkg}' must not be aliased`);
|
|
122
|
+
assert.equal(r.log, null, `DARWIN_BASE_DEPS entry '${pkg}' must not emit an alias log line`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
test("neo4j and cloudflared pass through unchanged — no alias hop", () => {
|
|
126
|
+
for (const pkg of DARWIN_SOLO_DEPS_FIXTURE) {
|
|
127
|
+
const r = decideBrewResolution({ pkg, brewInstalled: false });
|
|
128
|
+
assert.equal(r.resolved, pkg);
|
|
129
|
+
assert.equal(r.log, null);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
test("nodejs alias hop resolves to node@22 for darwin install", () => {
|
|
133
|
+
// The shared-dep call site (index.ts:988 installNodejs) passes "nodejs";
|
|
134
|
+
// this test pins the brew-formula landing for that single alias.
|
|
135
|
+
const r = decideBrewResolution({ pkg: "nodejs", brewInstalled: false });
|
|
136
|
+
assert.equal(r.resolved, "node@22");
|
|
137
|
+
assert.match(r.log ?? "", /node@22/);
|
|
138
|
+
});
|
|
@@ -128,6 +128,52 @@ test("renderPlist escapes `&` once, never double-escapes", () => {
|
|
|
128
128
|
// child to "" (current dir = launchd's cwd, /), which silently changes
|
|
129
129
|
// behaviour from "stay where invoked" to "/" — a different bug than absence.
|
|
130
130
|
// ---------------------------------------------------------------------------
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Brand-stamped labels — both shipping brands must render distinct labels so a
|
|
133
|
+
// co-resident install does not collide on a single LaunchAgent file.
|
|
134
|
+
// `launchdLabel()` in index.ts returns `com.rubytech.${BRAND.hostname}`; this
|
|
135
|
+
// test pins the brand-input → label contract end-to-end against the renderer.
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
test("renderPlist maxy-code brand fields land in the plist verbatim", () => {
|
|
138
|
+
// BRAND.hostname=maxy-code, configDir=.maxy-code per brands/maxy-code/brand.json.
|
|
139
|
+
// installServiceDarwin builds ProgramArguments=["/bin/bash", wrapperPath],
|
|
140
|
+
// WorkingDirectory=`${INSTALL_DIR}/server`, std{out,err}Path=`<logsDir>/server.log`.
|
|
141
|
+
const xml = renderPlist({
|
|
142
|
+
label: "com.rubytech.maxy-code",
|
|
143
|
+
programArguments: ["/bin/bash", "/Users/alice/.maxy-code/launchd-wrapper.sh"],
|
|
144
|
+
stdoutPath: "/Users/alice/.maxy-code/logs/server.log",
|
|
145
|
+
stderrPath: "/Users/alice/.maxy-code/logs/server.log",
|
|
146
|
+
keepAlive: true,
|
|
147
|
+
runAtLoad: true,
|
|
148
|
+
workingDirectory: "/Users/alice/maxy-code/server",
|
|
149
|
+
});
|
|
150
|
+
assert.match(xml, /<key>Label<\/key>\s*<string>com\.rubytech\.maxy-code<\/string>/);
|
|
151
|
+
assert.match(xml, /<string>\/bin\/bash<\/string>/);
|
|
152
|
+
assert.match(xml, /<string>\/Users\/alice\/\.maxy-code\/launchd-wrapper\.sh<\/string>/);
|
|
153
|
+
assert.match(xml, /<key>WorkingDirectory<\/key>\s*<string>\/Users\/alice\/maxy-code\/server<\/string>/);
|
|
154
|
+
assert.match(xml, /<key>StandardOutPath<\/key>\s*<string>\/Users\/alice\/\.maxy-code\/logs\/server\.log<\/string>/);
|
|
155
|
+
assert.match(xml, /<key>KeepAlive<\/key>\s*<true\/>/);
|
|
156
|
+
assert.match(xml, /<key>RunAtLoad<\/key>\s*<true\/>/);
|
|
157
|
+
});
|
|
158
|
+
test("renderPlist realagent-code brand fields land in the plist verbatim", () => {
|
|
159
|
+
// BRAND.hostname=realagent-code, configDir=.realagent-code per brands/realagent-code/brand.json.
|
|
160
|
+
// The label must differ from the maxy-code label so a co-resident install
|
|
161
|
+
// produces two distinct ~/Library/LaunchAgents/<label>.plist files.
|
|
162
|
+
const xml = renderPlist({
|
|
163
|
+
label: "com.rubytech.realagent-code",
|
|
164
|
+
programArguments: ["/bin/bash", "/Users/alice/.realagent-code/launchd-wrapper.sh"],
|
|
165
|
+
stdoutPath: "/Users/alice/.realagent-code/logs/server.log",
|
|
166
|
+
stderrPath: "/Users/alice/.realagent-code/logs/server.log",
|
|
167
|
+
keepAlive: true,
|
|
168
|
+
runAtLoad: true,
|
|
169
|
+
workingDirectory: "/Users/alice/realagent-code/server",
|
|
170
|
+
});
|
|
171
|
+
assert.match(xml, /<key>Label<\/key>\s*<string>com\.rubytech\.realagent-code<\/string>/);
|
|
172
|
+
assert.match(xml, /<string>\/Users\/alice\/\.realagent-code\/launchd-wrapper\.sh<\/string>/);
|
|
173
|
+
assert.match(xml, /<key>WorkingDirectory<\/key>\s*<string>\/Users\/alice\/realagent-code\/server<\/string>/);
|
|
174
|
+
// Label collision guard — the maxy-code label must not appear in a realagent plist.
|
|
175
|
+
assert.ok(!/com\.rubytech\.maxy-code/.test(xml), "realagent plist must not carry maxy-code label");
|
|
176
|
+
});
|
|
131
177
|
test("renderPlist omits WorkingDirectory key when workingDirectory is absent", () => {
|
|
132
178
|
const xml = renderPlist({
|
|
133
179
|
label: "com.rubytech.maxy",
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Static-grep contract for the darwin install branches in index.ts.
|
|
2
|
+
//
|
|
3
|
+
// Task 297 audited every `platform === "darwin"` block; this test pins the
|
|
4
|
+
// shape so a future refactor that removes a darwin branch without an explicit
|
|
5
|
+
// reason fails loudly here. The test reads src/index.ts at runtime (the
|
|
6
|
+
// authoritative source) and regex-asserts the per-branch markers — keeping
|
|
7
|
+
// the spec adjacent to the code rather than buried in a doc.
|
|
8
|
+
//
|
|
9
|
+
// Why static-grep, not behaviour: the install branches each spawn (sudo,
|
|
10
|
+
// brew, launchctl), and end-to-end darwin reproduction belongs to the manual
|
|
11
|
+
// smoke path in the task brief. The unit suite's job is to ensure the
|
|
12
|
+
// branch points still exist after a refactor.
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { dirname, resolve } from "node:path";
|
|
18
|
+
// dist/__tests__/macos-darwin-branch.test.js → ../../src/index.ts
|
|
19
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const INDEX_TS = resolve(here, "../../src/index.ts");
|
|
21
|
+
const SRC = readFileSync(INDEX_TS, "utf-8");
|
|
22
|
+
test("installSystemDeps has darwin branch that calls setMacosHostnameViaScutil on --hostname and scutil --get LocalHostName without", () => {
|
|
23
|
+
// The hostname block at index.ts:installSystemDeps must:
|
|
24
|
+
// - call setMacosHostnameViaScutil(HOSTNAME_FLAG) when HOSTNAME_FLAG is set
|
|
25
|
+
// - call scutil --get LocalHostName when HOSTNAME_FLAG is absent
|
|
26
|
+
// The two paths are the contract Task 297 locks in.
|
|
27
|
+
assert.match(SRC, /setMacosHostnameViaScutil\(HOSTNAME_FLAG\)/, "scutil-set path on --hostname must call setMacosHostnameViaScutil");
|
|
28
|
+
assert.match(SRC, /scutil["']?,\s*\[["']--get["']\s*,\s*["']LocalHostName["']\]/, "scutil --get LocalHostName must be the no-flag default on darwin");
|
|
29
|
+
assert.match(SRC, /darwin-hostname-mode=scutil-get/, "scutil-get success path must emit observability log");
|
|
30
|
+
assert.match(SRC, /darwin-hostname-mode=scutil-set/, "scutil-set path must emit observability log");
|
|
31
|
+
assert.match(SRC, /darwin-hostname-mode=brand-fallback/, "brand-fallback path must emit observability log");
|
|
32
|
+
});
|
|
33
|
+
test("installSystemDeps darwin branch installs DARWIN_BASE_DEPS via brew", () => {
|
|
34
|
+
// The shared dep set the darwin path feeds to installAllBrewPackages. If
|
|
35
|
+
// this list shrinks or a name changes, brew-resolve.test.ts (DARWIN_BASE_DEPS_FIXTURE)
|
|
36
|
+
// must move in lockstep — the comment in brew-resolve.test.ts says so.
|
|
37
|
+
assert.match(SRC, /DARWIN_BASE_DEPS\s*=\s*\[\s*"curl"\s*,\s*"git"\s*,\s*"unzip"\s*,\s*"jq"\s*,\s*"poppler"\s*,\s*"ffmpeg"\s*\]/);
|
|
38
|
+
assert.match(SRC, /installAllBrewPackages\(DARWIN_BASE_DEPS,\s*logFile\)/);
|
|
39
|
+
});
|
|
40
|
+
test("installCloudflared darwin branch logs the documented skip and brew-installs cloudflared", () => {
|
|
41
|
+
// Task 297 doctrine: skip tunnel provisioning entirely on darwin. The brew
|
|
42
|
+
// install puts the binary on PATH so the operator can opt in to `cloudflared
|
|
43
|
+
// tunnel login` post-install; the explicit log line is the diagnostic
|
|
44
|
+
// anchor for investigators grepping the install log.
|
|
45
|
+
assert.match(SRC, /darwin-cloudflare-skip=true/);
|
|
46
|
+
assert.match(SRC, /installAllBrewPackages\(\["cloudflared"\],\s*logFile\)/);
|
|
47
|
+
// No `cloudflared service install` or `cloudflared tunnel route dns`
|
|
48
|
+
// invocation in the installer surface — those are operator-driven,
|
|
49
|
+
// post-install steps. Match actual argv shapes (string-literal list to
|
|
50
|
+
// spawnSync/shell) rather than prose: argv arrays carry the args as
|
|
51
|
+
// quoted siblings inside `[...]`. Comment text is filtered by requiring
|
|
52
|
+
// a quote character immediately adjacent.
|
|
53
|
+
assert.ok(!/\[\s*["']cloudflared["'][^\]]*["']service["'][^\]]*["']install["']/.test(SRC), "installer must not invoke `cloudflared service install`");
|
|
54
|
+
assert.ok(!/\[\s*["']cloudflared["'][^\]]*["']tunnel["'][^\]]*["']route["'][^\]]*["']dns["']/.test(SRC), "installer must not invoke `cloudflared tunnel route dns`");
|
|
55
|
+
});
|
|
56
|
+
test("installService darwin branch delegates to installServiceDarwin (launchd path, not systemd)", () => {
|
|
57
|
+
// Two contracts: the platform switch routes darwin to installServiceDarwin,
|
|
58
|
+
// and installServiceDarwin writes the plist + bootstraps it via launchctl.
|
|
59
|
+
assert.match(SRC, /platform\s*===\s*"darwin"\s*\)\s*\{\s*installServiceDarwin\(\)/);
|
|
60
|
+
assert.match(SRC, /launchctl["']?,\s*\[["']bootstrap["']/);
|
|
61
|
+
assert.match(SRC, /launchd-plist=\$\{path\}\s+loaded=true/, "successful bootstrap must emit launchd-plist=… loaded=true");
|
|
62
|
+
assert.match(SRC, /launchd-plist=\$\{path\}\s+loaded=false/, "failed bootstrap must emit launchd-plist=… loaded=false");
|
|
63
|
+
});
|
|
64
|
+
test("installNodejs darwin branch brew-installs the `nodejs` alias name (resolver hops to node@22)", () => {
|
|
65
|
+
// The shared call site uses the apt-style `nodejs` name. brew-resolve.ts
|
|
66
|
+
// hops it to `node@22` via DARWIN_ALIASES — the alias is the deliberate
|
|
67
|
+
// wedge so a single dep name works across both backends.
|
|
68
|
+
assert.match(SRC, /platform\s*===\s*"darwin"[\s\S]{0,400}installAllBrewPackages\(\["nodejs"\]/);
|
|
69
|
+
});
|
|
70
|
+
test("installNeo4j darwin branch brew-installs `neo4j` (no alias hop)", () => {
|
|
71
|
+
// `neo4j` is the concrete brew formula name on darwin, no alias needed.
|
|
72
|
+
assert.match(SRC, /platform\s*===\s*"darwin"[\s\S]{0,800}installAllBrewPackages\(\["neo4j"\]/);
|
|
73
|
+
});
|
|
74
|
+
test("chromium binary-path file write skips darwin (Playwright-managed)", () => {
|
|
75
|
+
// writeChromiumBinaryPathFile early-returns on non-linux. The completion URL
|
|
76
|
+
// and VNC stack on darwin use Playwright/brew-managed Chromium instead of
|
|
77
|
+
// the linux per-brand chromium-binary.path file.
|
|
78
|
+
assert.match(SRC, /writeChromiumBinaryPathFile[\s\S]{0,200}process\.platform\s*!==\s*"linux"/);
|
|
79
|
+
});
|
|
80
|
+
test("uninstall darwin path bootsout the LaunchAgent and removes the plist", () => {
|
|
81
|
+
// The uninstall surface lives in uninstall.ts (separate file); this index.ts
|
|
82
|
+
// sanity check just confirms the launchdLabel helper still exists and is
|
|
83
|
+
// brand-keyed — install + uninstall both depend on the same label shape.
|
|
84
|
+
assert.match(SRC, /function launchdLabel\(\):\s*string\s*\{\s*return\s*`com\.rubytech\.\$\{BRAND\.hostname\}`/);
|
|
85
|
+
});
|
package/dist/brew-install.js
CHANGED
|
@@ -71,9 +71,14 @@ export function decideBrewInstallPlan(input) {
|
|
|
71
71
|
if (p.installed) {
|
|
72
72
|
const head = p.versions[0] ?? "?";
|
|
73
73
|
logs.push(` [homebrew] ${p.resolvedFormula} already installed (v${head})`);
|
|
74
|
+
// Uniform-prefix mirror line so the install-log grep recipe in
|
|
75
|
+
// platform/plugins/docs/references/deployment.md (`grep -E '^\[create-maxy\] …'`)
|
|
76
|
+
// covers every brew step alongside platform=, darwin-hostname-mode=, etc.
|
|
77
|
+
logs.push(` [create-maxy] brew-formula=${p.resolvedFormula} action=present`);
|
|
74
78
|
}
|
|
75
79
|
else {
|
|
76
80
|
logs.push(` [homebrew] installing ${p.resolvedFormula} …`);
|
|
81
|
+
logs.push(` [create-maxy] brew-formula=${p.resolvedFormula} action=install`);
|
|
77
82
|
toInstall.push(p.resolvedFormula);
|
|
78
83
|
}
|
|
79
84
|
}
|
package/dist/index.js
CHANGED
|
@@ -614,6 +614,7 @@ function installSystemDeps() {
|
|
|
614
614
|
// otherwise preserve the existing system hostname.
|
|
615
615
|
if (HOSTNAME_FLAG) {
|
|
616
616
|
console.log(` Hostname: ${HOSTNAME_FLAG} (from --hostname flag)`);
|
|
617
|
+
logFile(` [create-maxy] darwin-hostname-mode=scutil-set value=${HOSTNAME_FLAG}`);
|
|
617
618
|
try {
|
|
618
619
|
setMacosHostnameViaScutil(HOSTNAME_FLAG);
|
|
619
620
|
DEVICE_HOSTNAME = HOSTNAME_FLAG;
|
|
@@ -623,11 +624,25 @@ function installSystemDeps() {
|
|
|
623
624
|
}
|
|
624
625
|
}
|
|
625
626
|
else {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
627
|
+
// No flag — read the live LocalHostName so the printed URL matches mDNS
|
|
628
|
+
// resolution (`<LocalHostName>.local`). The `hostname` command on darwin
|
|
629
|
+
// returns the fully-qualified `<name>.local`, which would render the
|
|
630
|
+
// completion URL with a double `.local` suffix; scutil --get returns the
|
|
631
|
+
// bare token. No sudo, no system mutation.
|
|
632
|
+
const scutilGet = spawnSync("scutil", ["--get", "LocalHostName"], { encoding: "utf-8", stdio: "pipe", timeout: 5_000 });
|
|
633
|
+
const scutilName = ((scutilGet.stdout ?? "").trim());
|
|
634
|
+
if (scutilGet.status === 0 && scutilName.length > 0) {
|
|
635
|
+
DEVICE_HOSTNAME = scutilName;
|
|
636
|
+
console.log(` Hostname: ${DEVICE_HOSTNAME} (from scutil --get LocalHostName — no --hostname flag)`);
|
|
637
|
+
logFile(` [create-maxy] darwin-hostname-mode=scutil-get value=${DEVICE_HOSTNAME}`);
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
// scutil failed or returned empty — fall back to BRAND.hostname so the
|
|
641
|
+
// completion URL at least matches the brand's documented .local name.
|
|
642
|
+
DEVICE_HOSTNAME = BRAND.hostname;
|
|
643
|
+
console.log(` Hostname: ${DEVICE_HOSTNAME} (brand fallback — scutil --get LocalHostName empty)`);
|
|
644
|
+
logFile(` [create-maxy] darwin-hostname-mode=brand-fallback value=${DEVICE_HOSTNAME} scutil-status=${scutilGet.status}`);
|
|
629
645
|
}
|
|
630
|
-
catch { /* fallback to brand — set at declaration */ }
|
|
631
646
|
}
|
|
632
647
|
// macOS has no apt analogue for the VNC/WiFi-AP stacks
|
|
633
648
|
// (kiosk display + hostapd/dnsmasq are Pi-specific's
|
|
@@ -1541,8 +1556,11 @@ function installCloudflared() {
|
|
|
1541
1556
|
if (platform === "darwin") {
|
|
1542
1557
|
// Homebrew's `cloudflared` formula tracks the Cloudflare release stream;
|
|
1543
1558
|
// tunnel-login flow stays unchanged (`feedback_no_api_token_route.md`).
|
|
1544
|
-
// `cloudflared service install`
|
|
1545
|
-
//
|
|
1559
|
+
// `cloudflared service install` and `cloudflared tunnel route dns` are not
|
|
1560
|
+
// invoked by the installer on darwin — public reach is operator-driven
|
|
1561
|
+
// post-install (`cloudflared tunnel login` + follow-up tooling). The brew
|
|
1562
|
+
// install puts the binary on PATH so the operator can opt in later.
|
|
1563
|
+
logFile(` [create-maxy] darwin-cloudflare-skip=true reason="Mac laptop — no public hostname provisioned at install time"`);
|
|
1546
1564
|
installAllBrewPackages(["cloudflared"], logFile);
|
|
1547
1565
|
return;
|
|
1548
1566
|
}
|
|
@@ -2592,11 +2610,13 @@ function installServiceDarwin() {
|
|
|
2592
2610
|
if (bootstrap.status === 0) {
|
|
2593
2611
|
console.log(` [launchd] bootstrap ${gui()}/${label} ok`);
|
|
2594
2612
|
logFile(` [launchd] bootstrap ${gui()}/${label} ok`);
|
|
2613
|
+
logFile(` [create-maxy] launchd-plist=${path} loaded=true`);
|
|
2595
2614
|
}
|
|
2596
2615
|
else {
|
|
2597
2616
|
const stderr = (bootstrap.stderr ?? "").trim();
|
|
2598
2617
|
console.error(` [launchd] bootstrap returned ${bootstrap.status}: ${stderr}`);
|
|
2599
2618
|
logFile(` [launchd] bootstrap returned ${bootstrap.status}: ${stderr}`);
|
|
2619
|
+
logFile(` [create-maxy] launchd-plist=${path} loaded=false exit=${bootstrap.status}`);
|
|
2600
2620
|
throw new Error(`launchctl bootstrap ${gui()} ${path} failed (exit ${bootstrap.status}): ${stderr}`);
|
|
2601
2621
|
}
|
|
2602
2622
|
// Wait for the server to come up.
|
package/package.json
CHANGED
|
@@ -468,6 +468,111 @@ Removes the root-level systemd unit, `/etc/cloudflared/config.yml`, and the dupl
|
|
|
468
468
|
|
|
469
469
|
---
|
|
470
470
|
|
|
471
|
+
## Multi-ingress: adding hostnames for prototype services
|
|
472
|
+
|
|
473
|
+
Once the brand tunnel is up (admin + public + optionally SSH/SMB), additional service hostnames can be added without re-running the OAuth/cert/create-tunnel steps. This is the pattern the venture-studio `prototype-host` skill uses to expose hosted landing pages and wedge prototypes as `https://<surface>.<BRAND_HOSTNAME>`.
|
|
474
|
+
|
|
475
|
+
The flow is: append an ingress rule to `${CFG_DIR}/config.yml` immediately before the final `http_status:404` catch-all, validate the YAML, create the DNS row, restart the brand cloudflared service.
|
|
476
|
+
|
|
477
|
+
### 1. Append the ingress rule (awk, not sed)
|
|
478
|
+
|
|
479
|
+
`sed` cannot reliably insert before the last multi-line YAML rule. `awk` matches the literal catch-all line and inserts two lines before each match:
|
|
480
|
+
|
|
481
|
+
```
|
|
482
|
+
SURFACE="<surface>" # DNS label, e.g. landing or prototype
|
|
483
|
+
PORT="<port>" # local port the dev server / file server binds to
|
|
484
|
+
HOSTNAME="${SURFACE}.${BRAND_HOSTNAME}"
|
|
485
|
+
|
|
486
|
+
# Idempotency — skip if this hostname is already in config.yml.
|
|
487
|
+
if grep -qE "^[[:space:]]*-[[:space:]]+hostname:[[:space:]]+${HOSTNAME}\b" "${CFG_DIR}/config.yml"; then
|
|
488
|
+
echo "ingress-row-exists hostname=${HOSTNAME}"
|
|
489
|
+
else
|
|
490
|
+
awk -v host="${HOSTNAME}" -v port="${PORT}" '
|
|
491
|
+
/^[[:space:]]*-[[:space:]]+service:[[:space:]]+http_status:404[[:space:]]*$/ {
|
|
492
|
+
print " - hostname: " host
|
|
493
|
+
print " service: http://localhost:" port
|
|
494
|
+
}
|
|
495
|
+
{ print }
|
|
496
|
+
' "${CFG_DIR}/config.yml" > "${CFG_DIR}/config.yml.new"
|
|
497
|
+
mv "${CFG_DIR}/config.yml.new" "${CFG_DIR}/config.yml"
|
|
498
|
+
fi
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
After the edit the file looks like:
|
|
502
|
+
|
|
503
|
+
```yaml
|
|
504
|
+
tunnel: <TUNNEL_ID>
|
|
505
|
+
credentials-file: <CFG_DIR>/<TUNNEL_ID>.json
|
|
506
|
+
ingress:
|
|
507
|
+
- hostname: admin.<BRAND_HOSTNAME>
|
|
508
|
+
service: http://localhost:<PORT>
|
|
509
|
+
- hostname: public.<BRAND_HOSTNAME>
|
|
510
|
+
service: http://localhost:<PORT>
|
|
511
|
+
- hostname: <surface>.<BRAND_HOSTNAME>
|
|
512
|
+
service: http://localhost:<port>
|
|
513
|
+
- service: http_status:404
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
### 2. Validate
|
|
517
|
+
|
|
518
|
+
```
|
|
519
|
+
cloudflared --origincert "${CFG_DIR}/cert.pem" \
|
|
520
|
+
--config "${CFG_DIR}/config.yml" \
|
|
521
|
+
tunnel ingress validate
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Non-zero exit aborts the flow; cloudflared's stderr is the literal diagnostic.
|
|
525
|
+
|
|
526
|
+
### 3. Create the DNS row
|
|
527
|
+
|
|
528
|
+
```
|
|
529
|
+
TUNNEL_ID=$(jq -r '.id // .tunnelId' "${CFG_DIR}/tunnel.state")
|
|
530
|
+
cloudflared --origincert "${CFG_DIR}/cert.pem" \
|
|
531
|
+
tunnel route dns --overwrite-dns "${TUNNEL_ID}" "${HOSTNAME}"
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
`--overwrite-dns` replaces any existing CNAME for the hostname (common after a prior install on the same account). Without it, `tunnel route dns` errors `record already exists`.
|
|
535
|
+
|
|
536
|
+
### 4. Reload the connector
|
|
537
|
+
|
|
538
|
+
```
|
|
539
|
+
systemctl --user restart "${BRAND}-cloudflared.service"
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
Cloudflared does not watch `config.yml` for changes (the brand service unit launches it without `--config-watch`). Restart is the deterministic way to pick up the new rule; the brief ~1s connector blip is the same one every cloudflared upgrade incurs.
|
|
543
|
+
|
|
544
|
+
### Verification
|
|
545
|
+
|
|
546
|
+
```
|
|
547
|
+
curl -I https://<surface>.<BRAND_HOSTNAME>
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
Anything other than `HTTP/2 200` (or the dev server's natural 200/304 response) means either the rule didn't land, the systemd unit serving `<port>` isn't running, or DNS hasn't propagated. Run `cloudflared --origincert "${CFG_DIR}/cert.pem" --config "${CFG_DIR}/config.yml" tunnel ingress validate` and `dig <surface>.<BRAND_HOSTNAME>` to isolate which.
|
|
551
|
+
|
|
552
|
+
### Removing a prototype hostname
|
|
553
|
+
|
|
554
|
+
Manual for now (no scripted removal — the `prototype-host` skill does not yet ship a remove companion):
|
|
555
|
+
|
|
556
|
+
```
|
|
557
|
+
# 1. Stop and disable the service unit.
|
|
558
|
+
systemctl --user disable --now "${BRAND}-prototype-<surface>.service"
|
|
559
|
+
|
|
560
|
+
# 2. Strip the ingress row from config.yml — match the literal hostname line plus the following service line.
|
|
561
|
+
awk -v host="${SURFACE}.${BRAND_HOSTNAME}" '
|
|
562
|
+
$0 ~ "^[[:space:]]*-[[:space:]]+hostname:[[:space:]]+" host "[[:space:]]*$" { skip = 2; next }
|
|
563
|
+
skip > 0 { skip--; next }
|
|
564
|
+
{ print }
|
|
565
|
+
' "${CFG_DIR}/config.yml" > "${CFG_DIR}/config.yml.new" && mv "${CFG_DIR}/config.yml.new" "${CFG_DIR}/config.yml"
|
|
566
|
+
|
|
567
|
+
# 3. Delete the DNS row (dashboard, since the cloudflared CLI has no "route dns remove").
|
|
568
|
+
# Cloudflare → DNS → <zone> → delete CNAME for <surface>.<BRAND_HOSTNAME>.
|
|
569
|
+
|
|
570
|
+
# 4. Restart cloudflared.
|
|
571
|
+
systemctl --user restart "${BRAND}-cloudflared.service"
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
471
576
|
## Full reset
|
|
472
577
|
|
|
473
578
|
If everything is in a bad state and you want to start over. Assumes Step 0 has been run so `${BRAND}` and `${CFG_DIR}` are set.
|
|
@@ -9,15 +9,33 @@
|
|
|
9
9
|
|
|
10
10
|
## macOS install
|
|
11
11
|
|
|
12
|
-
On macOS the installer uses Homebrew + launchd instead of apt + systemd
|
|
12
|
+
On macOS the installer uses Homebrew + launchd instead of apt + systemd. No flags are required on a laptop:
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
npx -y @rubytech/create-maxy
|
|
15
|
+
npx -y @rubytech/create-maxy-code # default brand
|
|
16
|
+
npx -y @rubytech/create-realagent-code # realagent brand
|
|
16
17
|
```
|
|
17
18
|
|
|
18
|
-
**Prerequisite:** Homebrew. If `brew` is missing, the installer refuses with `Homebrew not found. Install from https://brew.sh and re-run.` Install Homebrew once via the official one-liner, then re-run.
|
|
19
|
+
**Prerequisite:** Homebrew. If `brew` is missing, the installer refuses with `Homebrew not found. Install from https://brew.sh and re-run.` Install Homebrew once via the official one-liner, then re-run. The installer never installs Homebrew itself.
|
|
20
|
+
|
|
21
|
+
**Hostname / printed URL.** Without `--hostname`, the installer reads `scutil --get LocalHostName` and prints the completion URL as `http://<that-name>.local:<port>`. No sudo, no system change — your Mac's existing local network name is what mDNS will resolve. With `--hostname <h>`, the installer sets `HostName` / `LocalHostName` / `ComputerName` to `<h>` via `sudo scutil` (one password prompt, all-or-nothing rollback within the three-call batch) so the URL becomes `http://<h>.local:<port>`. Grep `~/.<brand>/logs/install-*.log` for `[create-maxy] darwin-hostname-mode=` to confirm which path ran (`scutil-get`, `scutil-set`, or `brand-fallback`).
|
|
22
|
+
|
|
23
|
+
**LaunchAgent.** The installer registers `{{productName}}` as a launchd LaunchAgent at `~/Library/LaunchAgents/com.rubytech.<brand-hostname>.plist` — for example `com.rubytech.maxy-code.plist`. Survives logout/login and reboot via `KeepAlive=true` and `RunAtLoad=true`. Two brands on the same Mac get two distinct plists (brand-hostname-keyed), so install order is independent. Use `launchctl print gui/$UID/com.rubytech.<brand-hostname>` for service state. The `[create-maxy] launchd-plist=<path> loaded=true` line in the install log confirms `launchctl bootstrap` accepted the plist; `loaded=false exit=<n>` is the failure signal (run `plutil -lint <path>` to diagnose).
|
|
24
|
+
|
|
25
|
+
**Cloudflare on darwin.** The installer brew-installs the `cloudflared` binary so it is on PATH, but does not invoke `cloudflared service install` or `cloudflared tunnel route dns` — public reach is opt-in. After install, the operator runs `cloudflared tunnel login` (browser-driven) followed by the existing tunnel-setup flow if they want a public address. Grep `[create-maxy] darwin-cloudflare-skip=true` in the install log to confirm the installer took the documented skip path.
|
|
26
|
+
|
|
27
|
+
**Uninstall.** `npx -y @rubytech/create-maxy-code uninstall` (or the realagent equivalent) bootsout the LaunchAgent, removes the plist, and deletes `~/.<brand>/`. Homebrew-installed dependencies (curl, git, unzip, jq, poppler, ffmpeg, node@22, neo4j, cloudflared) remain — remove them with `brew uninstall` if you want a clean slate.
|
|
28
|
+
|
|
29
|
+
**Pre-flight.** macOS < 14 is refused at pre-flight via `parseSwVers` (`sw_vers -productVersion` must be `≥ 14`).
|
|
30
|
+
|
|
31
|
+
**Diagnostic grep recipe.** After a Mac install, the canonical log path is `~/.<brand>/logs/install-*.log`. One pass tells you everything:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
grep -E '^\[create-maxy\] (platform|darwin-hostname-mode|darwin-cloudflare-skip|launchd-plist|init-logging FAILED)=' \
|
|
35
|
+
~/.<brand>/logs/install-*.log
|
|
36
|
+
```
|
|
19
37
|
|
|
20
|
-
|
|
38
|
+
Every successful Mac install contains, in order: `platform=darwin`, `darwin-hostname-mode=…`, `darwin-cloudflare-skip=true`, every brew install/verify line, `launchd-plist=… loaded=true`. Absence of any of these is the failure signal.
|
|
21
39
|
|
|
22
40
|
## Initial Setup
|
|
23
41
|
|
|
@@ -39,6 +39,7 @@ import { graphPruneDenylistRemove } from "./tools/graph-prune-denylist-remove.js
|
|
|
39
39
|
import { conversationMemoryExpunge } from "./tools/conversation-memory-expunge.js";
|
|
40
40
|
import { conversationArchiveDeriveInsights, } from "./tools/conversation-archive-derive-insights.js";
|
|
41
41
|
import { conversationArchiveEnrichRejection } from "./tools/conversation-archive-enrich-rejection.js";
|
|
42
|
+
import { fetchImageAsContent } from "./tools/image-fetch.js";
|
|
42
43
|
import { getSession, closeDriver } from "./lib/neo4j.js";
|
|
43
44
|
import { embed } from "./lib/embeddings.js";
|
|
44
45
|
import { notTrashed } from "../../../../lib/graph-trash/dist/index.js";
|
|
@@ -1882,6 +1883,40 @@ eagerTool(server, "conversation-search", "Search conversation history using sema
|
|
|
1882
1883
|
};
|
|
1883
1884
|
}
|
|
1884
1885
|
});
|
|
1886
|
+
// image-fetch — return a remote image URL as a vision-capable content block.
|
|
1887
|
+
// Built for the listing-curator specialist's per-image vision pass; useful to
|
|
1888
|
+
// any caller that has an image URL in hand and a model that can see images.
|
|
1889
|
+
// Specialist tool, not in ADMIN_CORE_TOOLS — registered via server.tool, not
|
|
1890
|
+
// eagerTool (deferred-by-default ToolSearch tax is acceptable because the
|
|
1891
|
+
// curator is the only routine caller).
|
|
1892
|
+
server.tool("image-fetch", [
|
|
1893
|
+
"Fetch a remote image URL and return its bytes as an inline image content block the model can see.",
|
|
1894
|
+
"Guards: http/https only, content-type must start with image/, 10 MB byte cap, 30 s timeout.",
|
|
1895
|
+
"Returns one text block summarising the fetch plus one image block on success; one text block with isError on failure.",
|
|
1896
|
+
].join(" "), {
|
|
1897
|
+
url: z.string().describe("Absolute http(s) URL of the image to fetch."),
|
|
1898
|
+
}, async ({ url }) => {
|
|
1899
|
+
const result = await fetchImageAsContent(url);
|
|
1900
|
+
if (!result.ok) {
|
|
1901
|
+
return {
|
|
1902
|
+
content: [{ type: "text", text: `image-fetch failed: ${result.reason}` }],
|
|
1903
|
+
isError: true,
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
return {
|
|
1907
|
+
content: [
|
|
1908
|
+
{
|
|
1909
|
+
type: "text",
|
|
1910
|
+
text: `Fetched ${result.bytes} bytes (${result.mimeType}) from ${url}`,
|
|
1911
|
+
},
|
|
1912
|
+
{
|
|
1913
|
+
type: "image",
|
|
1914
|
+
data: result.base64,
|
|
1915
|
+
mimeType: result.mimeType,
|
|
1916
|
+
},
|
|
1917
|
+
],
|
|
1918
|
+
};
|
|
1919
|
+
});
|
|
1885
1920
|
// Cleanup on exit
|
|
1886
1921
|
process.on("SIGINT", async () => {
|
|
1887
1922
|
liveSchemaRuntime.cache.stop();
|