@pixel-point/toolcraft 0.0.8 → 0.0.11

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 (88) hide show
  1. package/README.md +42 -9
  2. package/package.json +1 -1
  3. package/src/generate.mjs +42 -5
  4. package/src/generate.test.mjs +40 -0
  5. package/src/package-json.mjs +15 -0
  6. package/src/package-json.test.mjs +14 -1
  7. package/templates/runtime/contracts/component-contracts.test.ts +251 -47
  8. package/templates/runtime/contracts/component-contracts.ts +130 -57
  9. package/templates/runtime/contracts/decision-contracts.test.ts +38 -1
  10. package/templates/runtime/contracts/decision-contracts.ts +22 -9
  11. package/templates/runtime/export/export.test.ts +63 -0
  12. package/templates/runtime/export/export.ts +55 -0
  13. package/templates/runtime/index.ts +1 -0
  14. package/templates/runtime/react/canvas-shell.test.tsx +58 -4
  15. package/templates/runtime/react/canvas-shell.tsx +31 -9
  16. package/templates/runtime/react/controls-panel.test.tsx +916 -108
  17. package/templates/runtime/react/controls-panel.tsx +136 -30
  18. package/templates/runtime/react/runtime-public-api.test.tsx +1 -1
  19. package/templates/runtime/react/settings-transfer.test.ts +4 -0
  20. package/templates/runtime/react/settings-transfer.ts +6 -1
  21. package/templates/runtime/react/timeline-panel.test.tsx +14 -0
  22. package/templates/runtime/react/timeline-panel.tsx +44 -7
  23. package/templates/runtime/react/toolcraft-app.integration.test.tsx +9 -1
  24. package/templates/runtime/react/toolcraft-app.test.tsx +112 -3
  25. package/templates/runtime/react/toolcraft-app.tsx +56 -37
  26. package/templates/runtime/schema/define-toolcraft.test.ts +266 -170
  27. package/templates/runtime/schema/define-toolcraft.ts +140 -246
  28. package/templates/runtime/schema/runtime-targets.ts +21 -0
  29. package/templates/runtime/schema/types.ts +44 -0
  30. package/templates/runtime/state/create-template-state.test.ts +156 -0
  31. package/templates/runtime/state/create-template-state.ts +38 -8
  32. package/templates/runtime/state/media-defaults.ts +105 -0
  33. package/templates/runtime/state/persistence.test.ts +58 -0
  34. package/templates/runtime/state/persistence.ts +105 -1
  35. package/templates/runtime/state/reducer.test.ts +280 -4
  36. package/templates/runtime/state/reducer.ts +195 -9
  37. package/templates/runtime/state/timeline-loop.test.ts +71 -0
  38. package/templates/runtime/state/timeline-loop.ts +35 -0
  39. package/templates/runtime/state/types.ts +27 -0
  40. package/templates/runtime/testing/performance.test.ts +810 -21
  41. package/templates/runtime/testing/performance.ts +823 -53
  42. package/templates/starter/AGENTS.md +24 -18
  43. package/templates/starter/docs/toolcraft/README.md +8 -4
  44. package/templates/starter/docs/toolcraft/acceptance-testing.md +43 -10
  45. package/templates/starter/docs/toolcraft/agent-worklog.md +1 -0
  46. package/templates/starter/docs/toolcraft/assembly-workflow.md +55 -20
  47. package/templates/starter/docs/toolcraft/component-rules.md +75 -42
  48. package/templates/starter/docs/toolcraft/decision-contract.md +2 -0
  49. package/templates/starter/docs/toolcraft/performance.md +37 -11
  50. package/templates/starter/docs/toolcraft/schema-reference.md +171 -41
  51. package/templates/starter/docs/toolcraft/workflow.md +5 -2
  52. package/templates/starter/e2e/app-browser-acceptance.spec.ts +3 -3
  53. package/templates/starter/e2e/app-performance.spec.ts +55 -2
  54. package/templates/starter/e2e/performance-helpers.ts +45 -0
  55. package/templates/starter/gitignore +1 -0
  56. package/templates/starter/index.html +1 -0
  57. package/templates/starter/package.json +3 -0
  58. package/templates/starter/playwright.config.ts +1 -1
  59. package/templates/starter/scripts/check-toolcraft-docs.mjs +1 -0
  60. package/templates/starter/scripts/run-vite-on-free-port.mjs +114 -13
  61. package/templates/starter/scripts/toolcraft-port.mjs +280 -0
  62. package/templates/starter/scripts/toolcraft-port.test.mjs +207 -1
  63. package/templates/starter/src/app/starter-acceptance.test.ts +3412 -479
  64. package/templates/starter/src/app/starter-acceptance.ts +1453 -97
  65. package/templates/starter/src/app/starter-performance.test.ts +111 -7
  66. package/templates/starter/src/app/starter-performance.ts +5 -0
  67. package/templates/starter/src/app/starter-schema.test.ts +32 -7
  68. package/templates/starter/src/app/starter-schema.ts +6 -2
  69. package/templates/starter/vite.config.ts +58 -2
  70. package/templates/ui/components/control-layout/index.tsx +8 -3
  71. package/templates/ui/components/controls/actions/actions-control.tsx +56 -5
  72. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +7 -3
  73. package/templates/ui/components/controls/color/index.ts +4 -1
  74. package/templates/ui/components/controls/color/palette-control.tsx +34 -4
  75. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +7 -2
  76. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +2 -2
  77. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +186 -14
  78. package/templates/ui/components/controls/file-drop/index.ts +6 -1
  79. package/templates/ui/components/controls/index.ts +4 -0
  80. package/templates/ui/components/controls/range-input/range-input-control.tsx +12 -4
  81. package/templates/ui/components/controls/range-slider/range-slider-value.ts +3 -1
  82. package/templates/ui/components/controls/select/select-control.tsx +8 -25
  83. package/templates/ui/components/controls/slider/slider-value.ts +0 -1
  84. package/templates/ui/components/controls/text-input/text-input-control.tsx +4 -1
  85. package/templates/ui/components/controls/vector/index.ts +1 -0
  86. package/templates/ui/components/controls/vector/vector-control.tsx +109 -12
  87. package/templates/ui/components/panel/panel-actions.tsx +1 -1
  88. package/templates/ui/components/panel/panel-section.tsx +29 -5
package/README.md CHANGED
@@ -1,6 +1,19 @@
1
1
  # Toolcraft CLI
2
2
 
3
- Package entrypoint for creating standalone Toolcraft apps.
3
+ Create standalone Toolcraft apps from the command line.
4
+
5
+ Toolcraft is a Pixel Point starter kit and UI/runtime library for building
6
+ personal creative design tools with AI. It gives generated apps a ready canvas,
7
+ control panel, export flow, AI workflow instructions, and verification checks so
8
+ you can focus prompts on the visual idea instead of rebuilding common creative
9
+ tool plumbing.
10
+
11
+ You use Toolcraft when a custom tool is faster or clearer than forcing a broad
12
+ design app to fit one specific job: procedural graphics, image effects, branded
13
+ asset generators, animation controllers, shader experiments, or quick visual
14
+ tools for client work.
15
+
16
+ ## Usage
4
17
 
5
18
  ```bash
6
19
  npx @pixel-point/toolcraft create
@@ -12,14 +25,20 @@ After dependencies are installed, Toolcraft installs the required workflow skill
12
25
  in a batch through the `skills` CLI. The skill installer uses the same agent,
13
26
  scope, and installation prompts as `npx skills add`.
14
27
 
15
- ## License
28
+ Example:
16
29
 
17
- Toolcraft is distributed under the Toolcraft Designer License in `LICENSE.md`.
18
- Designer client work is permitted under that license. Using AI coding assistants
19
- or agents such as Codex, Claude, ChatGPT, Cursor, or similar tools to work on
20
- generated apps is permitted. Platform, generator, AI software product,
21
- app-builder, website-builder, template-marketplace, and resale uses require a
22
- separate commercial license from Pixel Point.
30
+ ```bash
31
+ npx @pixel-point/toolcraft create my-ascii-tool
32
+ cd my-ascii-tool
33
+ pnpm dev
34
+ ```
35
+
36
+ Then open the generated folder in Codex, Claude Code, Cursor, or another AI
37
+ coding agent and prompt for the creative output you want, for example:
38
+
39
+ ```text
40
+ Build an app that applies an ASCII effect to an uploaded image.
41
+ ```
23
42
 
24
43
  Scripted usage:
25
44
 
@@ -50,5 +69,19 @@ Local tarball test, matching the published package layout:
50
69
  cd cli
51
70
  npm pack --pack-destination /tmp
52
71
  cd ..
53
- TOOLCRAFT_SKIP_INSTALL=1 TOOLCRAFT_SKIP_SKILLS=1 npm exec --package /tmp/pixel-point-toolcraft-0.0.2.tgz -- toolcraft create /tmp/toolcraft-pack-exec-test --name pack-exec-test --yes --force
72
+ TOOLCRAFT_SKIP_INSTALL=1 TOOLCRAFT_SKIP_SKILLS=1 npm exec --package /tmp/pixel-point-toolcraft-0.0.10.tgz -- toolcraft create /tmp/toolcraft-pack-exec-test --name pack-exec-test --yes --force
54
73
  ```
74
+
75
+ ## Learn more
76
+
77
+ - [How to craft personal design tools with AI](https://pixelpoint.io/blog/how-to-craft-personal-design-tools-with-toolcraft/)
78
+ - [Toolcraft YouTube tutorial](https://youtu.be/-QlmkGZLzFo)
79
+
80
+ ## License
81
+
82
+ Toolcraft is distributed under the Toolcraft Designer License in `LICENSE.md`.
83
+ Designer client work is permitted under that license. Using AI coding assistants
84
+ or agents such as Codex, Claude, ChatGPT, Cursor, or similar tools to work on
85
+ generated apps is permitted. Platform, generator, AI software product,
86
+ app-builder, website-builder, template-marketplace, and resale uses require a
87
+ separate commercial license from Pixel Point.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixel-point/toolcraft",
3
- "version": "0.0.8",
3
+ "version": "0.0.11",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "type": "module",
6
6
  "bin": {
package/src/generate.mjs CHANGED
@@ -11,7 +11,11 @@ import {
11
11
  pathExists,
12
12
  removeDirectory,
13
13
  } from "./copy-recursive.mjs";
14
- import { createGeneratedPackageJson, createGeneratedTsConfig } from "./package-json.mjs";
14
+ import {
15
+ createGeneratedPackageJson,
16
+ createGeneratedTsConfig,
17
+ normalizeProjectTitle,
18
+ } from "./package-json.mjs";
15
19
  import { rewriteGeneratedText, rewriteTextFiles } from "./rewrite-imports.mjs";
16
20
 
17
21
  const PACKAGE_ROOT = path.resolve(fileURLToPath(new URL("..", import.meta.url)));
@@ -21,6 +25,17 @@ function writeJson(filePath, value) {
21
25
  return fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
22
26
  }
23
27
 
28
+ function escapeHtmlText(value) {
29
+ return String(value)
30
+ .replaceAll("&", "&")
31
+ .replaceAll("<", "&lt;")
32
+ .replaceAll(">", "&gt;");
33
+ }
34
+
35
+ function escapeHtmlAttribute(value) {
36
+ return escapeHtmlText(value).replaceAll('"', "&quot;");
37
+ }
38
+
24
39
  async function readJson(filePath) {
25
40
  return JSON.parse(await fs.readFile(filePath, "utf8"));
26
41
  }
@@ -100,6 +115,25 @@ async function restoreGeneratedGitignore(targetDir) {
100
115
  await fs.rename(npmSafeGitignorePath, gitignorePath);
101
116
  }
102
117
 
118
+ async function writeGeneratedProjectTitle(targetDir, title) {
119
+ const indexPath = path.join(targetDir, "index.html");
120
+ const source = await fs.readFile(indexPath, "utf8");
121
+ const escapedTitle = escapeHtmlText(title);
122
+ const escapedTitleAttribute = escapeHtmlAttribute(title);
123
+ const withTitle = source.replace(/<title>.*?<\/title>/, `<title>${escapedTitle}</title>`);
124
+ const identityMeta = `<meta name="toolcraft-app-title" content="${escapedTitleAttribute}" />`;
125
+ const identityMetaPattern = /<meta\b(?=[^>]*\bname=["']toolcraft-app-title["'])[^>]*>/i;
126
+ const nextSource = identityMetaPattern.test(withTitle)
127
+ ? withTitle.replace(identityMetaPattern, identityMeta)
128
+ : withTitle.replace(/<head>/i, `<head>\n ${identityMeta}`);
129
+
130
+ if (!nextSource.includes(`<title>${escapedTitle}</title>`) || !nextSource.includes(identityMeta)) {
131
+ throw new Error(`Unable to update generated app title in ${indexPath}.`);
132
+ }
133
+
134
+ await fs.writeFile(indexPath, nextSource);
135
+ }
136
+
103
137
  async function removeToolcraftTestFiles(toolcraftRoot) {
104
138
  async function visit(currentDir) {
105
139
  const entries = await fs.readdir(currentDir, { withFileTypes: true });
@@ -190,6 +224,12 @@ export async function generateToolcraft(options = {}) {
190
224
  await assertDirectory(sourcePaths.uiSrc, "UI package source");
191
225
  await assertDirectory(sourcePaths.toolcraftSrc, "Toolcraft template runtime package source");
192
226
  const starterPackageJson = await readJson(path.join(sourcePaths.starterDir, "package.json"));
227
+ const packageJson = createGeneratedPackageJson({
228
+ name: options.name ?? path.basename(targetDir),
229
+ starterPackageJson,
230
+ });
231
+ const projectTitle = normalizeProjectTitle(packageJson.name);
232
+
193
233
  await ensureWritableTargetDirectory(targetDir, { force: options.force });
194
234
 
195
235
  await copyDirectory(sourcePaths.starterDir, targetDir);
@@ -206,11 +246,8 @@ export async function generateToolcraft(options = {}) {
206
246
  const changedFiles = await rewriteTextFiles(targetDir, (source) =>
207
247
  rewriteGeneratedAppText(source),
208
248
  );
249
+ await writeGeneratedProjectTitle(targetDir, projectTitle);
209
250
  await writeToolcraftIntegrityManifest(toolcraftRoot);
210
- const packageJson = createGeneratedPackageJson({
211
- name: options.name ?? path.basename(targetDir),
212
- starterPackageJson,
213
- });
214
251
 
215
252
  await writeJson(path.join(targetDir, "package.json"), packageJson);
216
253
  await writeJson(path.join(targetDir, "tsconfig.json"), createGeneratedTsConfig());
@@ -79,7 +79,15 @@ describe("generateToolcraft", () => {
79
79
  assert.equal(packageJson.devDependencies["@playwright/test"], "^1.51.1");
80
80
  assert.equal(packageJson.scripts["ai:check"], "node scripts/check-ai-skills.mjs");
81
81
  assert.equal(packageJson.scripts.dev, "node scripts/run-vite-on-free-port.mjs dev");
82
+ assert.equal(
83
+ packageJson.scripts["dev:restart"],
84
+ "node scripts/run-vite-on-free-port.mjs dev --toolcraft-restart",
85
+ );
82
86
  assert.equal(packageJson.scripts.preview, "node scripts/run-vite-on-free-port.mjs preview");
87
+ assert.equal(
88
+ packageJson.scripts["preview:restart"],
89
+ "node scripts/run-vite-on-free-port.mjs preview --toolcraft-restart",
90
+ );
83
91
  assert.equal(packageJson.scripts["docs:check"], "node scripts/check-toolcraft-docs.mjs");
84
92
  assert.equal(
85
93
  packageJson.scripts.test,
@@ -93,11 +101,16 @@ describe("generateToolcraft", () => {
93
101
  assert.equal(packageJson.scripts["verify:quick"], "pnpm ai:check && pnpm test");
94
102
  assert.equal(packageJson.scripts["verify:ui"], "pnpm test:browser");
95
103
  assert.equal(packageJson.scripts["verify:perf"], "pnpm test:browser:perf");
104
+ assert.equal(packageJson.scripts["verify:perf:playwright"], "pnpm test:browser:perf");
96
105
  assert.equal(
97
106
  packageJson.scripts["verify:final"],
98
107
  "pnpm ai:check && pnpm test && pnpm build && pnpm test:browser",
99
108
  );
100
109
 
110
+ const indexHtmlSource = await fs.readFile(path.join(targetDir, "index.html"), "utf8");
111
+ assert.match(indexHtmlSource, /<title>Generated App<\/title>/);
112
+ assert.match(indexHtmlSource, /<meta name="toolcraft-app-title" content="Generated App" \/>/);
113
+
101
114
  assert.ok(await fs.stat(path.join(targetDir, "playwright.config.ts")));
102
115
  const playwrightConfigSource = await fs.readFile(
103
116
  path.join(targetDir, "playwright.config.ts"),
@@ -121,6 +134,7 @@ describe("generateToolcraft", () => {
121
134
  assert.match(gitignoreSource, /dist/);
122
135
  assert.match(gitignoreSource, /playwright-report/);
123
136
  assert.match(gitignoreSource, /\.env\.\*/);
137
+ assert.match(gitignoreSource, /\.toolcraft/);
124
138
  await assert.rejects(() => fs.stat(path.join(targetDir, "gitignore")), /ENOENT/);
125
139
  assert.ok(await fs.stat(path.join(targetDir, "scripts/check-ai-skills.mjs")));
126
140
  assert.ok(await fs.stat(path.join(targetDir, "scripts/toolcraft-port.mjs")));
@@ -146,6 +160,9 @@ describe("generateToolcraft", () => {
146
160
  "utf8",
147
161
  );
148
162
  assert.match(appPerformanceSource, /defineToolcraftPerformance/);
163
+ assert.match(appPerformanceSource, /preferredRunner:\s*"agent-browser"/);
164
+ assert.match(appPerformanceSource, /fallbackRunner:\s*"playwright"/);
165
+ assert.match(appPerformanceSource, /agent-browser-unavailable/);
149
166
  assert.doesNotMatch(appPerformanceSource, /function\s+validateToolcraftPerformanceCoverage/);
150
167
  assert.doesNotMatch(
151
168
  appPerformanceSource,
@@ -325,4 +342,27 @@ describe("generateToolcraft", () => {
325
342
  /Target directory is not empty/,
326
343
  );
327
344
  });
345
+
346
+ it("writes the app identity marker when the generated title matches the template title", async () => {
347
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "toolcraft-cli-"));
348
+ tempRoots.push(tempRoot);
349
+
350
+ await generateToolcraft({
351
+ cwd: tempRoot,
352
+ force: true,
353
+ name: "toolcraft-app-template",
354
+ targetDir: "toolcraft-app-template",
355
+ });
356
+
357
+ const indexHtmlSource = await fs.readFile(
358
+ path.join(tempRoot, "toolcraft-app-template", "index.html"),
359
+ "utf8",
360
+ );
361
+
362
+ assert.match(indexHtmlSource, /<title>Toolcraft App Template<\/title>/);
363
+ assert.match(
364
+ indexHtmlSource,
365
+ /<meta name="toolcraft-app-title" content="Toolcraft App Template" \/>/,
366
+ );
367
+ });
328
368
  });
@@ -9,6 +9,21 @@ export function sanitizePackageName(value) {
9
9
  return sanitized || "toolcraft-app";
10
10
  }
11
11
 
12
+ export function normalizeProjectTitle(value) {
13
+ const words = String(value ?? "")
14
+ .trim()
15
+ .replace(/^@/, "")
16
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
17
+ .split(/[^a-zA-Z0-9]+/)
18
+ .filter(Boolean);
19
+
20
+ const title = words
21
+ .map((word) => `${word.charAt(0).toUpperCase()}${word.slice(1).toLowerCase()}`)
22
+ .join(" ");
23
+
24
+ return title || "Toolcraft App";
25
+ }
26
+
12
27
  function isWorkspaceDependency(name, specifier) {
13
28
  return name.startsWith("@repo/") || String(specifier).startsWith("workspace:");
14
29
  }
@@ -1,7 +1,11 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
3
 
4
- import { createGeneratedPackageJson, sanitizePackageName } from "./package-json.mjs";
4
+ import {
5
+ createGeneratedPackageJson,
6
+ normalizeProjectTitle,
7
+ sanitizePackageName,
8
+ } from "./package-json.mjs";
5
9
 
6
10
  describe("sanitizePackageName", () => {
7
11
  it("normalizes package names for generated apps", () => {
@@ -11,6 +15,15 @@ describe("sanitizePackageName", () => {
11
15
  });
12
16
  });
13
17
 
18
+ describe("normalizeProjectTitle", () => {
19
+ it("creates a human-readable title from generated project names", () => {
20
+ assert.equal(normalizeProjectTitle("mesh-gradient"), "Mesh Gradient");
21
+ assert.equal(normalizeProjectTitle("mesh_gradient.app"), "Mesh Gradient App");
22
+ assert.equal(normalizeProjectTitle("@pixel-point/mesh-gradient"), "Pixel Point Mesh Gradient");
23
+ assert.equal(normalizeProjectTitle(""), "Toolcraft App");
24
+ });
25
+ });
26
+
14
27
  describe("createGeneratedPackageJson", () => {
15
28
  it("uses the starter package manifest as source of truth", () => {
16
29
  const packageJson = createGeneratedPackageJson({