@sprig-and-prose/sprig 0.9.1 → 0.10.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +22 -129
  5. package/dist/cli.js.map +1 -1
  6. package/dist/compiler.d.ts +1 -3
  7. package/dist/compiler.d.ts.map +1 -1
  8. package/dist/compiler.js +30 -63
  9. package/dist/compiler.js.map +1 -1
  10. package/dist/diagnostics.d.ts +7 -0
  11. package/dist/diagnostics.d.ts.map +1 -0
  12. package/dist/diagnostics.js +69 -0
  13. package/dist/diagnostics.js.map +1 -0
  14. package/dist/prose.d.ts +1 -1
  15. package/dist/prose.d.ts.map +1 -1
  16. package/dist/prose.js +2 -5
  17. package/dist/prose.js.map +1 -1
  18. package/dist/ui.d.ts +1 -1
  19. package/dist/ui.d.ts.map +1 -1
  20. package/dist/ui.js +11 -12
  21. package/dist/ui.js.map +1 -1
  22. package/package.json +4 -7
  23. package/src/cli.ts +21 -168
  24. package/src/compiler.ts +40 -76
  25. package/src/diagnostics.ts +100 -0
  26. package/src/prose.ts +2 -5
  27. package/src/ui.ts +9 -12
  28. package/tests/compile.test.js +44 -17
  29. package/tests/compile.test.ts +65 -14
  30. package/tests/init.test.ts +8 -0
  31. package/tests/root.test.js +1 -1
  32. package/scripts/switch-deps.js +0 -44
  33. package/src/scene-compiler.ts +0 -192
  34. package/src/scene-discovery.ts +0 -51
  35. package/tests/compile-scene-only.test.js +0 -109
  36. package/tests/fixtures/scene-only-invalid/.sprig-out/bad.error.txt +0 -2
  37. package/tests/fixtures/scene-only-invalid/bad.scene.prose +0 -10
  38. package/tests/fixtures/scene-only-valid/.sprig-emit/AnotherScene.scene.json +0 -15
  39. package/tests/fixtures/scene-only-valid/.sprig-emit/SimpleScene.scene.json +0 -15
  40. package/tests/fixtures/scene-only-valid/.sprig-out/AnotherScene.scene.json +0 -15
  41. package/tests/fixtures/scene-only-valid/.sprig-out/SimpleScene.scene.json +0 -15
  42. package/tests/fixtures/scene-only-valid/another.scene.prose +0 -3
  43. package/tests/fixtures/scene-only-valid/simple.scene.prose +0 -3
  44. package/tests/fixtures/universe-unchanged/.sprig/TestScene.scene.json +0 -15
  45. package/tests/fixtures/universe-unchanged/custom-out/TestScene.scene.json +0 -15
  46. package/tests/fixtures/universe-unchanged/scene.scene.prose +0 -3
  47. package/tests/fixtures/universe-upstream/subdir/.sprig/OneScene.scene.json +0 -15
  48. package/tests/fixtures/universe-upstream/subdir/one.scene.prose +0 -3
@@ -1,44 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { readFileSync, writeFileSync } from 'fs';
4
- import { fileURLToPath } from 'url';
5
- import { dirname, join } from 'path';
6
- import { execSync } from 'child_process';
7
-
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = dirname(__filename);
10
- const packageJsonPath = join(__dirname, '..', 'package.json');
11
-
12
- const mode = process.argv[2]; // 'local' or 'published'
13
-
14
- if (!mode || !['local', 'published'].includes(mode)) {
15
- console.error('Usage: node scripts/switch-deps.js [local|published]');
16
- process.exit(1);
17
- }
18
-
19
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
20
-
21
- if (mode === 'local') {
22
- // Switch to local file: protocol
23
- packageJson.dependencies['@sprig-and-prose/sprig-universe'] = 'file:../sprig-universe';
24
- packageJson.dependencies['@sprig-and-prose/sprig-ui-csr'] = 'file:../sprig-ui-csr';
25
- console.log('✓ Switched to local dependencies');
26
- } else {
27
- // Fetch latest published versions from npm
28
- console.log('Fetching latest published versions...');
29
- try {
30
- const universeVersion = execSync('npm view @sprig-and-prose/sprig-universe version', { encoding: 'utf-8' }).trim();
31
- const uiVersion = execSync('npm view @sprig-and-prose/sprig-ui-csr version', { encoding: 'utf-8' }).trim();
32
-
33
- packageJson.dependencies['@sprig-and-prose/sprig-universe'] = `^${universeVersion}`;
34
- packageJson.dependencies['@sprig-and-prose/sprig-ui-csr'] = `^${uiVersion}`;
35
- console.log(`✓ Switched to published dependencies (universe: ^${universeVersion}, ui-csr: ^${uiVersion})`);
36
- } catch (error) {
37
- console.error('Error fetching latest versions from npm:', error.message);
38
- process.exit(1);
39
- }
40
- }
41
-
42
- writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
43
- console.log('✓ Updated package.json');
44
-
@@ -1,192 +0,0 @@
1
- import { readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from "node:fs";
2
- import { join, basename } from "node:path";
3
- import { execSync } from "node:child_process";
4
- import fg from "fast-glob";
5
- // Import directly from compile_scene to avoid scene-engine dependency
6
- // @ts-expect-error - sprig-scenes doesn't have full TypeScript types
7
- import { compileSceneFromText } from "@sprig-and-prose/sprig-scenes/compiler/compile_scene";
8
-
9
- export interface SceneCompileResult {
10
- success: boolean;
11
- scenes: string[];
12
- diagnostics: Array<{ severity: string; message: string; source?: unknown; scene?: string }>;
13
- }
14
-
15
- /** Base name for output files derived from scene prose path (e.g. 05-group.scene.prose → 05-group). */
16
- function sceneFileBaseName(sceneFile: string): string {
17
- return basename(sceneFile).replace(/\.scene\.prose$/i, "") || "scene";
18
- }
19
-
20
- /**
21
- * Write compile errors to <base>.error.txt in outputDir.
22
- * One line per error, then " at <scene-name>" if scene name was parsed from source, else " in <filename>".
23
- */
24
- function writeSceneErrors(
25
- outputDir: string,
26
- sceneFile: string,
27
- errors: string[],
28
- sourceText: string,
29
- ): void {
30
- const base = sceneFileBaseName(sceneFile);
31
- const sceneNameMatch = sourceText.match(/\bscene\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/);
32
- const locationLine = sceneNameMatch
33
- ? " at " + sceneNameMatch[1] + "\n"
34
- : " in " + base + "\n";
35
- const errorPath = join(outputDir, `${base}.error.txt`);
36
- const errorBlock = errors.join("\n").trim() + (errors.length ? "\n" : "");
37
- const content = errorBlock + locationLine;
38
- writeFileSync(errorPath, content, "utf-8");
39
- }
40
-
41
- /**
42
- * Generate manifestId: git:<sha> if available, else time:<iso>
43
- */
44
- function generateManifestId(): string {
45
- try {
46
- const sha = execSync("git rev-parse HEAD", { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
47
- if (sha) {
48
- return `git:${sha}`;
49
- }
50
- } catch {
51
- // Git not available or not in a git repo
52
- }
53
- return `time:${new Date().toISOString()}`;
54
- }
55
-
56
- export interface CompileScenesOptions {
57
- /** Explicit scene file paths (skips globbing) */
58
- sceneFiles?: string[];
59
- /** If false, parse and validate only; do not write manifests or error files */
60
- emitManifests?: boolean;
61
- }
62
-
63
- /**
64
- * Compile all scene files in the universe
65
- * @param universeRoot - Universe root directory (or search root for scene-only mode)
66
- * @param outputDir - Output directory for scene manifests
67
- * @param options - Optional: sceneFiles (explicit list), emitManifests (default true in universe mode)
68
- * @returns Compile result with success status and list of compiled scenes
69
- */
70
- export async function compileScenes(
71
- universeRoot: string,
72
- outputDir: string,
73
- options?: CompileScenesOptions,
74
- ): Promise<SceneCompileResult> {
75
- const diagnostics: Array<{ severity: string; message: string; source?: unknown; scene?: string }> = [];
76
- const compiledScenes: string[] = [];
77
- const emitManifests = options?.emitManifests !== false;
78
-
79
- try {
80
- // Use explicit scene files or discover via glob
81
- let sceneFiles: string[];
82
- if (options?.sceneFiles && options.sceneFiles.length > 0) {
83
- sceneFiles = options.sceneFiles;
84
- } else {
85
- sceneFiles = await fg("**/*.scene.prose", {
86
- cwd: universeRoot,
87
- absolute: true,
88
- ignore: ["node_modules/**", ".git/**"],
89
- });
90
- }
91
-
92
- if (sceneFiles.length === 0) {
93
- // No scenes to compile is not an error
94
- return {
95
- success: true,
96
- scenes: [],
97
- diagnostics: [],
98
- };
99
- }
100
-
101
- if (emitManifests) {
102
- // Ensure output directory exists
103
- try {
104
- mkdirSync(outputDir, { recursive: true });
105
- } catch (error) {
106
- // Directory might already exist, ignore
107
- }
108
- }
109
-
110
- const generatedAt = new Date().toISOString();
111
- const manifestId = emitManifests ? generateManifestId() : "";
112
-
113
- // Compile each scene
114
- for (const sceneFile of sceneFiles) {
115
- let sourceText = "";
116
- try {
117
- sourceText = readFileSync(sceneFile, "utf-8");
118
- const manifest = compileSceneFromText(sourceText);
119
-
120
- if (!manifest || !manifest.sceneName) {
121
- const message = "Failed to compile scene: no sceneName found";
122
- diagnostics.push({
123
- severity: "error",
124
- message,
125
- source: sceneFile,
126
- });
127
- if (emitManifests) {
128
- writeSceneErrors(outputDir, sceneFile, [message], sourceText);
129
- }
130
- continue;
131
- }
132
-
133
- compiledScenes.push(manifest.sceneName);
134
-
135
- if (emitManifests) {
136
- // Inject metadata
137
- const manifestWithMeta = {
138
- ...manifest,
139
- meta: {
140
- generatedAt,
141
- manifestId,
142
- },
143
- };
144
-
145
- // Write to output directory
146
- const outputPath = join(outputDir, `${manifest.sceneName}.scene.json`);
147
- const tempPath = `${outputPath}.tmp`;
148
- const json = JSON.stringify(manifestWithMeta, null, 2);
149
- writeFileSync(tempPath, json, "utf-8");
150
- renameSync(tempPath, outputPath);
151
-
152
- // Remove any prior .error.txt for this scene (same source file base name)
153
- const errorPath = join(outputDir, `${sceneFileBaseName(sceneFile)}.error.txt`);
154
- try {
155
- unlinkSync(errorPath);
156
- } catch {
157
- // ignore if missing
158
- }
159
- }
160
- } catch (error) {
161
- const message = error instanceof Error ? error.message : String(error);
162
- diagnostics.push({
163
- severity: "error",
164
- message: `Scene compile failed: ${message}`,
165
- source: sceneFile,
166
- });
167
- if (emitManifests) {
168
- writeSceneErrors(outputDir, sceneFile, [message], sourceText);
169
- }
170
- }
171
- }
172
-
173
- const hasErrors = diagnostics.some((d) => d.severity === "error");
174
- return {
175
- success: !hasErrors,
176
- scenes: compiledScenes,
177
- diagnostics,
178
- };
179
- } catch (error) {
180
- const message = error instanceof Error ? error.message : String(error);
181
- return {
182
- success: false,
183
- scenes: [],
184
- diagnostics: [
185
- {
186
- severity: "error",
187
- message: `Scene discovery error: ${message}`,
188
- },
189
- ],
190
- };
191
- }
192
- }
@@ -1,51 +0,0 @@
1
- import { statSync, existsSync } from "node:fs";
2
- import { resolve, dirname, join } from "node:path";
3
- import fg from "fast-glob";
4
-
5
- export interface SceneDiscovery {
6
- sceneFiles: string[];
7
- searchRoot: string;
8
- }
9
-
10
- /**
11
- * Discover scene files when no universe.prose is found.
12
- * @param path - Path (file or directory) from user
13
- * @returns SceneDiscovery with sceneFiles and searchRoot, or null if no scenes found
14
- */
15
- export function discoverSceneFiles(path: string): SceneDiscovery | null {
16
- const resolved = resolve(path);
17
-
18
- if (!existsSync(resolved)) {
19
- return null;
20
- }
21
-
22
- const stat = statSync(resolved);
23
-
24
- if (stat.isFile()) {
25
- if (/\.scene\.prose$/i.test(resolved)) {
26
- return {
27
- sceneFiles: [resolved],
28
- searchRoot: dirname(resolved),
29
- };
30
- }
31
- // Other file: search in same directory
32
- const searchRoot = dirname(resolved);
33
- const sceneFiles = fg.sync("**/*.scene.prose", {
34
- cwd: searchRoot,
35
- absolute: true,
36
- ignore: ["node_modules/**", ".git/**"],
37
- });
38
- return sceneFiles.length > 0 ? { sceneFiles, searchRoot } : null;
39
- }
40
-
41
- if (stat.isDirectory()) {
42
- const sceneFiles = fg.sync("**/*.scene.prose", {
43
- cwd: resolved,
44
- absolute: true,
45
- ignore: ["node_modules/**", ".git/**"],
46
- });
47
- return sceneFiles.length > 0 ? { sceneFiles, searchRoot: resolved } : null;
48
- }
49
-
50
- return null;
51
- }
@@ -1,109 +0,0 @@
1
- import { test } from "node:test";
2
- import { strict as assert } from "node:assert";
3
- import { existsSync } from "node:fs";
4
- import { join, dirname } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
- import { spawnSync } from "node:child_process";
7
-
8
- const __dirname = dirname(fileURLToPath(import.meta.url));
9
- const FIXTURES = join(__dirname, "fixtures");
10
- const CLI_PATH = join(__dirname, "..", "dist", "cli.js");
11
-
12
- function runCompile(args, cwd) {
13
- const result = spawnSync("node", [CLI_PATH, "compile", ...args], {
14
- encoding: "utf-8",
15
- cwd: cwd ?? join(__dirname, ".."),
16
- });
17
- return {
18
- status: result.status,
19
- stdout: result.stdout ?? "",
20
- stderr: result.stderr ?? "",
21
- };
22
- }
23
-
24
- test("sprig compile with no universe in cwd: errors", () => {
25
- const cwd = join(FIXTURES, "scene-only-empty");
26
- const { status, stderr } = runCompile([], cwd);
27
-
28
- assert.notStrictEqual(status, 0, "Expected non-zero exit");
29
- assert.ok(stderr.includes("No universe.prose found."), "Should show expected error");
30
- });
31
-
32
- test("sprig compile <dir> universe in dir, no --out: writes to dir/.sprig", () => {
33
- const dir = join(FIXTURES, "universe-unchanged");
34
- const { status, stderr } = runCompile([dir]);
35
-
36
- assert.strictEqual(status, 0, `Expected exit 0, got ${status}. stderr: ${stderr}`);
37
-
38
- const sprigDir = join(dir, ".sprig");
39
- assert.ok(existsSync(sprigDir), ".sprig should exist");
40
- assert.ok(existsSync(join(sprigDir, "manifest.json")), "manifest.json should exist");
41
- assert.ok(existsSync(join(sprigDir, "TestScene.scene.json")), "TestScene.scene.json should exist");
42
- });
43
-
44
- test("sprig compile <dir> --out <out> universe found: writes to out", () => {
45
- const dir = join(FIXTURES, "universe-unchanged");
46
- const outDir = join(dir, "custom-out");
47
- const { status, stderr } = runCompile([dir, "--out", outDir]);
48
-
49
- assert.strictEqual(status, 0, `Expected exit 0, got ${status}. stderr: ${stderr}`);
50
-
51
- assert.ok(existsSync(join(outDir, "manifest.json")), "manifest.json should exist in --out");
52
- assert.ok(existsSync(join(outDir, "TestScene.scene.json")), "TestScene.scene.json should exist in --out");
53
- });
54
-
55
- test("sprig compile <dir> no universe, no --out: errors with guidance", () => {
56
- const dir = join(FIXTURES, "scene-only-valid");
57
- const { status, stderr } = runCompile([dir]);
58
-
59
- assert.notStrictEqual(status, 0, "Expected non-zero exit");
60
- assert.ok(
61
- stderr.includes("No universe.prose found for"),
62
- "Should mention no universe for dir",
63
- );
64
- assert.ok(stderr.includes("Provide --out"), "Should suggest --out");
65
- });
66
-
67
- test("sprig compile <dir> --out <out> no universe, scenes present: emits to out", () => {
68
- const dir = join(FIXTURES, "scene-only-valid");
69
- const outDir = join(dir, ".sprig-out");
70
- const { status, stderr } = runCompile([dir, "--out", outDir]);
71
-
72
- assert.strictEqual(status, 0, `Expected exit 0, got ${status}. stderr: ${stderr}`);
73
-
74
- assert.ok(existsSync(join(outDir, "SimpleScene.scene.json")), "SimpleScene.scene.json should exist");
75
- assert.ok(existsSync(join(outDir, "AnotherScene.scene.json")), "AnotherScene.scene.json should exist");
76
- });
77
-
78
- test("sprig compile <dir> --out <out> no universe, no scenes: errors", () => {
79
- const dir = join(FIXTURES, "scene-only-empty");
80
- const outDir = join(dir, ".sprig-out");
81
- const { status, stderr } = runCompile([dir, "--out", outDir]);
82
-
83
- assert.notStrictEqual(status, 0, "Expected non-zero exit");
84
- assert.ok(
85
- stderr.includes("No universe.prose found and no *.scene.prose files found in:"),
86
- "Should show no scenes error",
87
- );
88
- });
89
-
90
- test("sprig compile <dir> universe in parent, no --out: writes to in-directory/.sprig", () => {
91
- const subdir = join(FIXTURES, "universe-upstream", "subdir");
92
- const { status, stderr } = runCompile([subdir]);
93
-
94
- assert.strictEqual(status, 0, `Expected exit 0, got ${status}. stderr: ${stderr}`);
95
-
96
- const sprigDir = join(subdir, ".sprig");
97
- assert.ok(existsSync(sprigDir), "Output should be in subdir/.sprig (in-directory)");
98
- assert.ok(existsSync(join(sprigDir, "manifest.json")), "manifest.json should exist");
99
- assert.ok(existsSync(join(sprigDir, "OneScene.scene.json")), "OneScene.scene.json should exist");
100
- });
101
-
102
- test("scene-only invalid dir with --out: exits non-zero", () => {
103
- const dir = join(FIXTURES, "scene-only-invalid");
104
- const outDir = join(dir, ".sprig-out");
105
- const { status, stderr } = runCompile([dir, "--out", outDir]);
106
-
107
- assert.notStrictEqual(status, 0, "Expected non-zero exit on invalid scene");
108
- assert.ok(stderr.includes("BadScene") || stderr.includes("bad.scene.prose"), "Error should mention scene or file");
109
- });
@@ -1,2 +0,0 @@
1
- Field 'id' may only define a single type (e.g. integer, string, array, or an actor). This error appears because two or more types are defined for this field.
2
- at BadScene
@@ -1,10 +0,0 @@
1
- scene BadScene {
2
- actor Actor {
3
- shape {
4
- id {
5
- integer
6
- string
7
- }
8
- }
9
- }
10
- }
@@ -1,15 +0,0 @@
1
- {
2
- "sceneName": "AnotherScene",
3
- "actors": {
4
- "NameActor": {
5
- "type": "string",
6
- "identity": []
7
- }
8
- },
9
- "portals": {},
10
- "derived": {},
11
- "meta": {
12
- "generatedAt": "2026-02-21T20:00:48.348Z",
13
- "manifestId": "git:e97c34ae2cb10839cafef399d1ef329411e3bd22"
14
- }
15
- }
@@ -1,15 +0,0 @@
1
- {
2
- "sceneName": "SimpleScene",
3
- "actors": {
4
- "IntActor": {
5
- "type": "integer",
6
- "identity": []
7
- }
8
- },
9
- "portals": {},
10
- "derived": {},
11
- "meta": {
12
- "generatedAt": "2026-02-21T20:00:48.348Z",
13
- "manifestId": "git:e97c34ae2cb10839cafef399d1ef329411e3bd22"
14
- }
15
- }
@@ -1,15 +0,0 @@
1
- {
2
- "sceneName": "AnotherScene",
3
- "actors": {
4
- "NameActor": {
5
- "type": "string",
6
- "identity": []
7
- }
8
- },
9
- "portals": {},
10
- "derived": {},
11
- "meta": {
12
- "generatedAt": "2026-02-21T20:23:33.528Z",
13
- "manifestId": "git:e97c34ae2cb10839cafef399d1ef329411e3bd22"
14
- }
15
- }
@@ -1,15 +0,0 @@
1
- {
2
- "sceneName": "SimpleScene",
3
- "actors": {
4
- "IntActor": {
5
- "type": "integer",
6
- "identity": []
7
- }
8
- },
9
- "portals": {},
10
- "derived": {},
11
- "meta": {
12
- "generatedAt": "2026-02-21T20:23:33.528Z",
13
- "manifestId": "git:e97c34ae2cb10839cafef399d1ef329411e3bd22"
14
- }
15
- }
@@ -1,3 +0,0 @@
1
- scene AnotherScene {
2
- actor NameActor { string }
3
- }
@@ -1,3 +0,0 @@
1
- scene SimpleScene {
2
- actor IntActor { integer }
3
- }
@@ -1,15 +0,0 @@
1
- {
2
- "sceneName": "TestScene",
3
- "actors": {
4
- "IntActor": {
5
- "type": "integer",
6
- "identity": []
7
- }
8
- },
9
- "portals": {},
10
- "derived": {},
11
- "meta": {
12
- "generatedAt": "2026-02-21T20:23:33.122Z",
13
- "manifestId": "git:e97c34ae2cb10839cafef399d1ef329411e3bd22"
14
- }
15
- }
@@ -1,15 +0,0 @@
1
- {
2
- "sceneName": "TestScene",
3
- "actors": {
4
- "IntActor": {
5
- "type": "integer",
6
- "identity": []
7
- }
8
- },
9
- "portals": {},
10
- "derived": {},
11
- "meta": {
12
- "generatedAt": "2026-02-21T20:23:33.279Z",
13
- "manifestId": "git:e97c34ae2cb10839cafef399d1ef329411e3bd22"
14
- }
15
- }
@@ -1,3 +0,0 @@
1
- scene TestScene {
2
- actor IntActor { integer }
3
- }
@@ -1,15 +0,0 @@
1
- {
2
- "sceneName": "OneScene",
3
- "actors": {
4
- "IntActor": {
5
- "type": "integer",
6
- "identity": []
7
- }
8
- },
9
- "portals": {},
10
- "derived": {},
11
- "meta": {
12
- "generatedAt": "2026-02-21T20:23:33.807Z",
13
- "manifestId": "git:e97c34ae2cb10839cafef399d1ef329411e3bd22"
14
- }
15
- }
@@ -1,3 +0,0 @@
1
- scene OneScene {
2
- actor IntActor { integer }
3
- }