@1agh/maude 0.47.0 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/apps/studio/acp/bridge.ts +8 -1
- package/apps/studio/acp/plugin-bootstrap.ts +15 -1
- package/apps/studio/build.ts +1 -1
- package/apps/studio/client/export-center.jsx +7 -6
- package/apps/studio/client/github.js +11 -4
- package/apps/studio/dist/client.bundle.js +282 -282
- package/apps/studio/dist/runtime/REMOTION-LICENSE.md +1 -1
- package/apps/studio/examples/perf-100-artboards.tsx +1 -1
- package/apps/studio/exporters/pdf.ts +35 -1
- package/apps/studio/git/service.ts +4 -1
- package/apps/studio/paths.ts +37 -1
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +34 -0
- package/apps/studio/test/acp-session-plugins.test.ts +6 -0
- package/apps/studio/test/exporters/pdf.test.ts +33 -1
- package/cli/bin/maude.mjs +1 -0
- package/cli/commands/init.mjs +80 -3
- package/cli/commands/kg.mjs +368 -0
- package/cli/commands/kg.test.mjs +118 -0
- package/cli/lib/ddr-to-kgai.mjs +648 -0
- package/cli/lib/ddr-to-kgai.test.mjs +99 -0
- package/cli/lib/flow-design-integration.test.mjs +2 -2
- package/cli/lib/gitignore-block.mjs +4 -0
- package/cli/lib/plugin-name-namespace.test.mjs +71 -0
- package/package.json +8 -8
- package/plugins/design/dependencies.json +17 -0
- package/plugins/flow/.claude-plugin/config.schema.json +66 -0
- package/plugins/flow/dependencies.json +17 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Maude's **video-comp** canvas kind bundles the Remotion runtime
|
|
4
4
|
(`remotion`, `@remotion/player`, `@remotion/transitions`) into the pre-built
|
|
5
5
|
`/_canvas-runtime/*.js` bundles in this directory. See
|
|
6
|
-
[DDR-148](../../../.ai/decisions/DDR-148-video-comp-remotion-authoring-capture-export.md).
|
|
6
|
+
[DDR-148](../../../.ai/archive/decisions/DDR-148-video-comp-remotion-authoring-capture-export.md).
|
|
7
7
|
|
|
8
8
|
**Remotion is source-available software — it is NOT MIT-licensed.** It is
|
|
9
9
|
free for individuals and for companies of up to 3 people (unlimited commercial
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* 3. Zoom around the cursor with wheel for 5 s; record FPS.
|
|
18
18
|
* 4. Compare against the Phase 3.4 perf budget targets.
|
|
19
19
|
*
|
|
20
|
-
* Results land in .ai/decisions/DDR-024 + .ai/logs/phase-4-perf-<date>.md.
|
|
20
|
+
* Results land in .ai/archive/decisions/DDR-024 + .ai/logs/phase-4-perf-<date>.md.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { DCArtboard, DCSection, DesignCanvas } from '@maude/canvas-lib';
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
// deviceScaleFactor. `resolveDeviceScale` (exporters/png.ts) is reused here
|
|
34
34
|
// unchanged so `dpi` means the same physical resolution in both exporters.
|
|
35
35
|
|
|
36
|
-
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
36
|
+
import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
37
37
|
import { tmpdir } from 'node:os';
|
|
38
38
|
import path from 'node:path';
|
|
39
39
|
|
|
@@ -79,6 +79,39 @@ function resolveSourceFileUnderRoot(repoRoot: string, sourceFile: string): strin
|
|
|
79
79
|
return abs;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// RCA issue-desktop-print-pdf-save-as-hang-large-payload: a print-DPI
|
|
83
|
+
// "canvas as separate" export re-embeds full-size photos independently per
|
|
84
|
+
// page (Chromium's page.pdf() has no cross-page image-object dedup, unlike a
|
|
85
|
+
// single hand-authored PDF), so several photo-heavy artboards at print DPI
|
|
86
|
+
// can legitimately reach hundreds of MB. That's not inherently a bug — a
|
|
87
|
+
// real multi-page print-shop deliverable can genuinely be that big — so this
|
|
88
|
+
// guard exists to fail loud on a RUNAWAY size, not to cap ordinary print
|
|
89
|
+
// output. The desktop Save… path no longer chokes on a large payload either
|
|
90
|
+
// way (apps/desktop/src-tauri/src/lib.rs streams it disk-to-disk), so the
|
|
91
|
+
// remaining risk this guards is server-side: pdf-lib's `out.save()` below
|
|
92
|
+
// must hold the whole assembled document in memory, and the browser (non-
|
|
93
|
+
// desktop) download path still loads the full blob into the tab. Reuses
|
|
94
|
+
// png.ts's own MAX_OUTPUT_BYTES ceiling (_png-playwright.mjs) rather than a
|
|
95
|
+
// fresh number — same "generous RGBA-buffer-scale ceiling, well under
|
|
96
|
+
// typical CI/desktop RAM" rationale (DDR-182) applies to a large assembled
|
|
97
|
+
// PDF just as much as a large raster buffer. Measured on-disk after capture
|
|
98
|
+
// (a PDF's output size, unlike a raster export's, isn't predictable from
|
|
99
|
+
// width×height×scale alone). If a legitimate export outgrows this, raise it
|
|
100
|
+
// deliberately rather than treating the number as sacred.
|
|
101
|
+
export const MAX_TOTAL_OUTPUT_BYTES = 600 * 1024 * 1024;
|
|
102
|
+
export function assertTotalSizeOk(paths: string[]): void {
|
|
103
|
+
let total = 0;
|
|
104
|
+
for (const p of paths) total += statSync(p).size;
|
|
105
|
+
if (total > MAX_TOTAL_OUTPUT_BYTES) {
|
|
106
|
+
const mb = Math.round(total / 1024 / 1024);
|
|
107
|
+
const maxMb = Math.round(MAX_TOTAL_OUTPUT_BYTES / 1024 / 1024);
|
|
108
|
+
throw new Error(
|
|
109
|
+
`Print PDF export is too large (~${mb}MB, limit ~${maxMb}MB) — try a lower ` +
|
|
110
|
+
'DPI, exporting fewer artboards at once, or exporting artboards as separate files.'
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
82
115
|
async function capturePdf(
|
|
83
116
|
target: Extract<Target, { kind: 'element' }>,
|
|
84
117
|
ctx: ExportContext,
|
|
@@ -352,6 +385,7 @@ export async function run(
|
|
|
352
385
|
if (!written.length) {
|
|
353
386
|
return { filename: 'export.pdf', contentType: 'application/pdf', body: new Uint8Array(0) };
|
|
354
387
|
}
|
|
388
|
+
assertTotalSizeOk(written.map((w) => w.path));
|
|
355
389
|
|
|
356
390
|
const baseSlug = elementTargets[0]?.canvasSlug ?? 'export';
|
|
357
391
|
const needsPostPass = !!printOpts || !!pageFit;
|
|
@@ -215,7 +215,10 @@ function isMaudeRuntimeState(p: string): boolean {
|
|
|
215
215
|
/(^|\/)_server\.(?:lock|log)$/.test(p) ||
|
|
216
216
|
/(^|\/)_(?:history|trash|draw|photo|smoke|canvas-state|state|chat|comments|untrusted|export-jobs)(?:\/|$)/.test(
|
|
217
217
|
p
|
|
218
|
-
)
|
|
218
|
+
) ||
|
|
219
|
+
// kgai per-machine graph projection (feature-kgai-ecosystem-integration,
|
|
220
|
+
// DDR-115 taxonomy) — the append-only store rebuilds from the remote on sync.
|
|
221
|
+
/(^|\/)\.kgai(?:\/|$)/.test(p)
|
|
219
222
|
);
|
|
220
223
|
}
|
|
221
224
|
|
package/apps/studio/paths.ts
CHANGED
|
@@ -89,7 +89,10 @@ export const MEDIA_DIR: string = join(DEV_SERVER_ROOT, 'media');
|
|
|
89
89
|
* the resolver skips injection there (those users have a terminal + the manual
|
|
90
90
|
* marketplace path). Only the dev tree and the staged desktop bundle carry it.
|
|
91
91
|
*/
|
|
92
|
-
export function pluginDirFrom(
|
|
92
|
+
export function pluginDirFrom(
|
|
93
|
+
devServerRoot: string,
|
|
94
|
+
plugin: 'design' | 'flow' | 'kgai'
|
|
95
|
+
): string | null {
|
|
93
96
|
const dir = join(devServerRoot, '..', '..', 'plugins', plugin);
|
|
94
97
|
return existsSync(join(dir, '.claude-plugin', 'plugin.json')) ? dir : null;
|
|
95
98
|
}
|
|
@@ -100,6 +103,39 @@ export const DESIGN_PLUGIN_DIR: string | null = pluginDirFrom(DEV_SERVER_ROOT, '
|
|
|
100
103
|
/** Bundled `flow` plugin tree, or `null` (npm/web layout). See {@link pluginDirFrom}. */
|
|
101
104
|
export const FLOW_PLUGIN_DIR: string | null = pluginDirFrom(DEV_SERVER_ROOT, 'flow');
|
|
102
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Bundled THIRD-PARTY `kgai` plugin tree (kgaidev/kgai, MIT), or `null`.
|
|
108
|
+
*
|
|
109
|
+
* Staged only by the desktop build (`apps/desktop/scripts/sync-kg.mjs` fetches a
|
|
110
|
+
* PINNED upstream release — never vendored, never floating), so this is non-null
|
|
111
|
+
* exclusively in the packaged `.app`. It carries kgai's `Stop` hook, which is the
|
|
112
|
+
* autonomous decision-capture nudge; without injecting this plugin the ACP chat
|
|
113
|
+
* panel captures nothing (the terminal-less DDR-177 user never marketplace-installs
|
|
114
|
+
* kgai). The upstream `SessionStart` install hook is stripped at stage time.
|
|
115
|
+
*/
|
|
116
|
+
export const KGAI_PLUGIN_DIR: string | null = pluginDirFrom(DEV_SERVER_ROOT, 'kgai');
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The staged kgai engine: `{ bin, lib }` absolute paths, or `null` when this
|
|
120
|
+
* layout doesn't ship it (dev tree / npm — where a user-installed `kg` on PATH
|
|
121
|
+
* is used instead). `bin` is the Tauri `externalBin` sidecar (signed + notarized
|
|
122
|
+
* with the app); `lib` is the directory holding `libkuzu`, exported as `KGAI_LIB`
|
|
123
|
+
* so the spawn folds it into DYLD_/LD_LIBRARY_PATH (`cli/commands/kg.mjs`).
|
|
124
|
+
*
|
|
125
|
+
* Resolved from DEV_SERVER_ROOT per DDR-045 (never `import.meta.url`).
|
|
126
|
+
*/
|
|
127
|
+
export function resolveStagedKgai(): { bin: string; lib: string } | null {
|
|
128
|
+
// Sidecars land beside the app executable; resources under Contents/Resources.
|
|
129
|
+
// <Resources>/apps/studio → <Resources> → ../MacOS/<sidecar>
|
|
130
|
+
const resources = join(DEV_SERVER_ROOT, '..', '..');
|
|
131
|
+
const lib = join(resources, 'kgai');
|
|
132
|
+
if (!existsSync(lib)) return null;
|
|
133
|
+
for (const candidate of [join(resources, '..', 'MacOS', 'kg'), join(resources, 'kgai', 'kg')]) {
|
|
134
|
+
if (existsSync(candidate)) return { bin: candidate, lib };
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
103
139
|
/**
|
|
104
140
|
* Whether we are running inside a `bun --compile` standalone binary
|
|
105
141
|
* (true when `import.meta.url` resolves to bun's virtual filesystem).
|
|
@@ -16,6 +16,7 @@ import { computeSessionPlugins } from '../acp/plugin-bootstrap.ts';
|
|
|
16
16
|
|
|
17
17
|
const DESIGN = '/bundle/plugins/design';
|
|
18
18
|
const FLOW = '/bundle/plugins/flow';
|
|
19
|
+
const KGAI = '/bundle/plugins/kgai';
|
|
19
20
|
|
|
20
21
|
describe('computeSessionPlugins — gates', () => {
|
|
21
22
|
// `/flow` auto-load is intentionally OFF for now (2026-07-03) — only `design`
|
|
@@ -52,4 +53,37 @@ describe('computeSessionPlugins — gates', () => {
|
|
|
52
53
|
const out = computeSessionPlugins({ native: true, designDir: DESIGN, flowDir: FLOW });
|
|
53
54
|
expect(out.every((c) => c.skipMcpDiscovery === true && c.type === 'local')).toBe(true);
|
|
54
55
|
});
|
|
56
|
+
|
|
57
|
+
// feature-kgai-ecosystem-integration Phase 8 — the third-party kgai plugin
|
|
58
|
+
// carries the `Stop` hook that IS autonomous decision capture. Without
|
|
59
|
+
// injection the packaged app records nothing (settingSources:['user'], and a
|
|
60
|
+
// terminal-less DDR-177 user never marketplace-installs it).
|
|
61
|
+
test('kgai bundled ⇒ injected alongside design (autonomous capture in the .app)', () => {
|
|
62
|
+
const out = computeSessionPlugins({
|
|
63
|
+
native: true,
|
|
64
|
+
designDir: DESIGN,
|
|
65
|
+
flowDir: FLOW,
|
|
66
|
+
kgaiDir: KGAI,
|
|
67
|
+
});
|
|
68
|
+
expect(out).toEqual([
|
|
69
|
+
{ type: 'local', path: DESIGN, skipMcpDiscovery: true },
|
|
70
|
+
{ type: 'local', path: KGAI, skipMcpDiscovery: true },
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('kgai absent (dev tree / npm layout) ⇒ design only, no crash', () => {
|
|
75
|
+
expect(
|
|
76
|
+
computeSessionPlugins({ native: true, designDir: DESIGN, flowDir: FLOW, kgaiDir: null })
|
|
77
|
+
).toEqual([{ type: 'local', path: DESIGN, skipMcpDiscovery: true }]);
|
|
78
|
+
// omitted entirely (optional field) behaves the same
|
|
79
|
+
expect(computeSessionPlugins({ native: true, designDir: DESIGN, flowDir: FLOW })).toEqual([
|
|
80
|
+
{ type: 'local', path: DESIGN, skipMcpDiscovery: true },
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('not native ⇒ kgai is NOT injected either (web-serve gate holds)', () => {
|
|
85
|
+
expect(
|
|
86
|
+
computeSessionPlugins({ native: false, designDir: DESIGN, flowDir: FLOW, kgaiDir: KGAI })
|
|
87
|
+
).toEqual([]);
|
|
88
|
+
});
|
|
55
89
|
});
|
|
@@ -94,6 +94,12 @@ describe('newSessionParams — plugin carrier shape', () => {
|
|
|
94
94
|
const p = newSessionParams('/repo', undefined, PLUGINS);
|
|
95
95
|
expect((p._meta as Meta).claudeCode?.options?.settings?.enabledPlugins).toEqual({
|
|
96
96
|
'design@maude': false,
|
|
97
|
+
// kgai (third-party, injected by the desktop bundle for autonomous capture —
|
|
98
|
+
// feature-kgai-ecosystem-integration Phase 8). Suppressing a user-installed
|
|
99
|
+
// copy matters MORE here than for our own plugins: it would run its own
|
|
100
|
+
// SessionStart install.sh and point at a different engine version than the
|
|
101
|
+
// pinned, signed sidecar we ship.
|
|
102
|
+
kgai: false,
|
|
97
103
|
});
|
|
98
104
|
});
|
|
99
105
|
|
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
// via a synthetic single-target run that short-circuits screenshot.
|
|
7
7
|
|
|
8
8
|
import { describe, expect, test } from 'bun:test';
|
|
9
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
9
12
|
|
|
10
13
|
import { PDFDocument } from 'pdf-lib';
|
|
11
|
-
import { run } from '../../exporters/pdf.ts';
|
|
14
|
+
import { assertTotalSizeOk, MAX_TOTAL_OUTPUT_BYTES, run } from '../../exporters/pdf.ts';
|
|
12
15
|
|
|
13
16
|
const CTX = {
|
|
14
17
|
designRoot: '/tmp/.design',
|
|
@@ -31,6 +34,35 @@ describe('pdf adapter — contract', () => {
|
|
|
31
34
|
});
|
|
32
35
|
});
|
|
33
36
|
|
|
37
|
+
describe('pdf adapter — total output-size guard (RCA issue-desktop-print-pdf-save-as-hang-large-payload)', () => {
|
|
38
|
+
test('passes when total bytes are under the ceiling', () => {
|
|
39
|
+
const tmp = mkdtempSync(path.join(tmpdir(), 'maude-pdf-guard-'));
|
|
40
|
+
try {
|
|
41
|
+
const p = path.join(tmp, 'a.pdf');
|
|
42
|
+
writeFileSync(p, Buffer.alloc(1024));
|
|
43
|
+
expect(() => assertTotalSizeOk([p])).not.toThrow();
|
|
44
|
+
} finally {
|
|
45
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('throws an actionable error when total bytes exceed the ceiling', () => {
|
|
50
|
+
const tmp = mkdtempSync(path.join(tmpdir(), 'maude-pdf-guard-'));
|
|
51
|
+
try {
|
|
52
|
+
// Two files, each just over half the ceiling — sums past the limit
|
|
53
|
+
// without needing to actually allocate a giant single buffer.
|
|
54
|
+
const half = Math.ceil(MAX_TOTAL_OUTPUT_BYTES / 2) + 1024;
|
|
55
|
+
const a = path.join(tmp, 'a.pdf');
|
|
56
|
+
const b = path.join(tmp, 'b.pdf');
|
|
57
|
+
writeFileSync(a, Buffer.alloc(half));
|
|
58
|
+
writeFileSync(b, Buffer.alloc(half));
|
|
59
|
+
expect(() => assertTotalSizeOk([a, b])).toThrow(/too large/i);
|
|
60
|
+
} finally {
|
|
61
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
34
66
|
describe('pdf-lib smoke — embed + save', () => {
|
|
35
67
|
test('embeds a PNG and produces a valid PDF buffer', async () => {
|
|
36
68
|
const pdf = await PDFDocument.create();
|
package/cli/bin/maude.mjs
CHANGED
|
@@ -18,6 +18,7 @@ const COMMANDS = {
|
|
|
18
18
|
design: () => import('../commands/design.mjs'),
|
|
19
19
|
'scenario-report': () => import('../commands/scenario-report.mjs'),
|
|
20
20
|
doctor: () => import('../commands/doctor.mjs'),
|
|
21
|
+
kg: () => import('../commands/kg.mjs'),
|
|
21
22
|
help: () => import('../commands/help.mjs'),
|
|
22
23
|
hub: () => import('../commands/hub.mjs'),
|
|
23
24
|
version: () => import('../commands/version.mjs'),
|
package/cli/commands/init.mjs
CHANGED
|
@@ -1,8 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { stat, writeFile } from 'node:fs/promises';
|
|
2
4
|
import { basename, resolve } from 'node:path';
|
|
3
5
|
import { parseArgs } from '../lib/argv.mjs';
|
|
4
6
|
import { copyTree } from '../lib/copy-tree.mjs';
|
|
5
7
|
|
|
8
|
+
// Thin STATE.md written under --kg: the knowledge graph is the history authority,
|
|
9
|
+
// so STATE.md shrinks to a human breadcrumb (Open fork #2 — stub, not removal).
|
|
10
|
+
const KG_STATE_STUB = `# Workflow State
|
|
11
|
+
|
|
12
|
+
> **kgai-active repo** — decision history + working context live in the knowledge graph, not this file.
|
|
13
|
+
> The \`flow:workflow-state\` skill reads/writes the graph via \`flow:kgai-backend\`.
|
|
14
|
+
|
|
15
|
+
**Status:** ready
|
|
16
|
+
**Active plan:** —
|
|
17
|
+
|
|
18
|
+
## Where the history went
|
|
19
|
+
|
|
20
|
+
- **Decisions / "why is X so":** \`maude kg context --about "<area>"\`
|
|
21
|
+
- **Recent movements:** \`maude kg query "MATCH (d:Decision) WHERE d.author='<you>' RETURN d.title, d.recorded_at ORDER BY d.recorded_at DESC LIMIT 10"\`
|
|
22
|
+
- **Conflicts:** \`maude kg conflicts\`
|
|
23
|
+
|
|
24
|
+
The old \`.ai/decisions/\` archive (if any) is preserved read-only — never auto-deleted.
|
|
25
|
+
`;
|
|
26
|
+
|
|
6
27
|
const PLACEHOLDER = 'PROJECT_NAME';
|
|
7
28
|
// Files in the skeleton that contain the project-name placeholder and should
|
|
8
29
|
// be templated on copy.
|
|
@@ -43,10 +64,12 @@ const CHANGELOG_STUBS = {
|
|
|
43
64
|
const VALID_PROVIDERS = new Set(['changesets', 'git-cliff', 'conventional', 'custom', 'none']);
|
|
44
65
|
|
|
45
66
|
export async function run({ args, pkgRoot }) {
|
|
46
|
-
const { flags } = parseArgs(args, { booleans: ['force', 'dry-run', 'help'] });
|
|
67
|
+
const { flags } = parseArgs(args, { booleans: ['force', 'dry-run', 'help', 'kg'] });
|
|
47
68
|
if (flags.help) {
|
|
48
69
|
process.stdout.write(
|
|
49
|
-
'maude init [--name <project>] [--provider <changesets|git-cliff|conventional|custom|none>] [--force] [--dry-run]\n'
|
|
70
|
+
'maude init [--name <project>] [--provider <changesets|git-cliff|conventional|custom|none>] [--kg] [--force] [--dry-run]\n' +
|
|
71
|
+
' --kg opt into the kgai knowledge-graph backend: write a thin STATE.md pointer-stub\n' +
|
|
72
|
+
' and bootstrap a local store via `kg init` (no-op when `kg` is not installed).\n'
|
|
50
73
|
);
|
|
51
74
|
return;
|
|
52
75
|
}
|
|
@@ -136,10 +159,64 @@ export async function run({ args, pkgRoot }) {
|
|
|
136
159
|
(await pathExists(resolve(cwd, 'CLAUDE.md'))) ||
|
|
137
160
|
(await pathExists(resolve(cwd, '.claude', 'CLAUDE.md')));
|
|
138
161
|
|
|
162
|
+
// --kg (opt-in): the knowledge graph becomes the history authority. Replace the
|
|
163
|
+
// scaffolded STATE.md with a thin pointer-stub and bootstrap a local store.
|
|
164
|
+
// The `knowledgeGraph` config block stays ABSENT (bias-free skeleton ⇒ auto via
|
|
165
|
+
// the schema default — onboarding / `maude doctor --fix` fills store + scope).
|
|
166
|
+
if (flags.kg && !flags['dry-run']) {
|
|
167
|
+
const statePath = resolve(aiDir, 'state', 'STATE.md');
|
|
168
|
+
await writeFile(statePath, KG_STATE_STUB, 'utf8');
|
|
169
|
+
process.stdout.write(' kgai: wrote thin STATE.md pointer-stub (history lives in the graph)\n');
|
|
170
|
+
const kgBin = resolveKgBin();
|
|
171
|
+
if (kgBin) {
|
|
172
|
+
// No --root: kgai defaults the store to `<cwd>/.kgai/store` (which the
|
|
173
|
+
// resolver auto-detects and the gitignore block ignores). Passing --root
|
|
174
|
+
// would place the store's loose files at cwd root instead.
|
|
175
|
+
const r = spawnSync(kgBin, ['init'], {
|
|
176
|
+
cwd,
|
|
177
|
+
stdio: 'ignore',
|
|
178
|
+
env: kgInitEnv(),
|
|
179
|
+
});
|
|
180
|
+
process.stdout.write(
|
|
181
|
+
r.status === 0
|
|
182
|
+
? ' kgai: bootstrapped local store via `kg init` (git-author actor captured)\n'
|
|
183
|
+
: ' kgai: `kg init` did not complete — run it manually (see docs/kgai-onboarding.md)\n'
|
|
184
|
+
);
|
|
185
|
+
} else {
|
|
186
|
+
process.stdout.write(
|
|
187
|
+
' kgai: `kg` not installed — store not bootstrapped. See docs/kgai-onboarding.md.\n'
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
process.stdout.write(
|
|
191
|
+
' kgai: set `knowledgeGraph.store` + `scope` per docs/kgai-onboarding.md\n'
|
|
192
|
+
);
|
|
193
|
+
} else if (flags.kg) {
|
|
194
|
+
process.stdout.write(' kgai: (dry-run) would write STATE.md stub + bootstrap the store\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
139
197
|
printSummary(result);
|
|
140
198
|
printNextSteps(projectName, claudeMdExists);
|
|
141
199
|
}
|
|
142
200
|
|
|
201
|
+
/** KGAI_BIN (desktop-staged sidecar) → `kg` on PATH → null. Mirrors kg.mjs. */
|
|
202
|
+
function resolveKgBin() {
|
|
203
|
+
if (process.env.KGAI_BIN && existsSync(process.env.KGAI_BIN)) return process.env.KGAI_BIN;
|
|
204
|
+
const probe = spawnSync('sh', ['-c', 'command -v kg'], { encoding: 'utf8' });
|
|
205
|
+
const found = (probe.stdout || '').trim();
|
|
206
|
+
return probe.status === 0 && found ? found : null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Fold KGAI_LIB into DYLD_LIBRARY_PATH so a staged libkuzu resolves (desktop). */
|
|
210
|
+
function kgInitEnv() {
|
|
211
|
+
const env = { ...process.env };
|
|
212
|
+
if (process.env.KGAI_LIB) {
|
|
213
|
+
env.DYLD_LIBRARY_PATH = [process.env.KGAI_LIB, process.env.DYLD_LIBRARY_PATH]
|
|
214
|
+
.filter(Boolean)
|
|
215
|
+
.join(':');
|
|
216
|
+
}
|
|
217
|
+
return env;
|
|
218
|
+
}
|
|
219
|
+
|
|
143
220
|
function isValidName(s) {
|
|
144
221
|
return /^[a-z0-9._-]+$/i.test(s);
|
|
145
222
|
}
|