@mondaydotcomorg/z2h-cli 0.1.0 → 0.1.1
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/commands/__tests__/create-workspace.test.d.ts +2 -0
- package/dist/commands/__tests__/create-workspace.test.d.ts.map +1 -0
- package/dist/commands/__tests__/create.test.d.ts +2 -0
- package/dist/commands/__tests__/create.test.d.ts.map +1 -0
- package/dist/commands/create-workspace.d.ts +5 -0
- package/dist/commands/create-workspace.d.ts.map +1 -0
- package/dist/commands/create-workspace.js +80 -0
- package/dist/commands/create.d.ts +7 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +97 -0
- package/dist/constants.d.ts +1 -14
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +11 -34
- package/dist/esm/commands/__tests__/create-workspace.test.d.ts +2 -0
- package/dist/esm/commands/__tests__/create-workspace.test.d.ts.map +1 -0
- package/dist/esm/commands/__tests__/create.test.d.ts +2 -0
- package/dist/esm/commands/__tests__/create.test.d.ts.map +1 -0
- package/dist/esm/commands/create-workspace.d.ts +5 -0
- package/dist/esm/commands/create-workspace.d.ts.map +1 -0
- package/dist/esm/commands/create-workspace.mjs +74 -0
- package/dist/esm/commands/create.d.ts +7 -0
- package/dist/esm/commands/create.d.ts.map +1 -0
- package/dist/esm/commands/create.mjs +91 -0
- package/dist/esm/constants.d.ts +1 -14
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.mjs +11 -32
- package/dist/esm/index.mjs +28 -0
- package/dist/esm/shadow/__tests__/consumer-pkg.test.d.ts +2 -0
- package/dist/esm/shadow/__tests__/consumer-pkg.test.d.ts.map +1 -0
- package/dist/esm/shadow/__tests__/merge-package-json.test.d.ts +2 -0
- package/dist/esm/shadow/__tests__/merge-package-json.test.d.ts.map +1 -0
- package/dist/esm/shadow/consumer-pkg.d.ts +5 -15
- package/dist/esm/shadow/consumer-pkg.d.ts.map +1 -1
- package/dist/esm/shadow/consumer-pkg.mjs +11 -72
- package/dist/esm/shadow/merge-package-json.d.ts +10 -4
- package/dist/esm/shadow/merge-package-json.d.ts.map +1 -1
- package/dist/esm/shadow/merge-package-json.mjs +12 -11
- package/dist/esm/shadow/sync.mjs +2 -2
- package/dist/esm/templates/app/claude-settings.mjs +10 -0
- package/dist/esm/types.d.ts +0 -1
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/util/__tests__/templates.test.d.ts +2 -0
- package/dist/esm/util/__tests__/templates.test.d.ts.map +1 -0
- package/dist/esm/util/__tests__/workspace-templates-shipped.test.d.ts +2 -0
- package/dist/esm/util/__tests__/workspace-templates-shipped.test.d.ts.map +1 -0
- package/dist/esm/util/__tests__/workspace.test.d.ts +2 -0
- package/dist/esm/util/__tests__/workspace.test.d.ts.map +1 -0
- package/dist/esm/util/s3/upload.mjs +1 -1
- package/dist/esm/util/templates.d.ts +22 -0
- package/dist/esm/util/templates.d.ts.map +1 -0
- package/dist/esm/util/templates.mjs +36 -0
- package/dist/esm/util/workspace.d.ts +20 -0
- package/dist/esm/util/workspace.d.ts.map +1 -0
- package/dist/esm/util/workspace.mjs +46 -0
- package/dist/index.js +28 -0
- package/dist/shadow/__tests__/consumer-pkg.test.d.ts +2 -0
- package/dist/shadow/__tests__/consumer-pkg.test.d.ts.map +1 -0
- package/dist/shadow/__tests__/merge-package-json.test.d.ts +2 -0
- package/dist/shadow/__tests__/merge-package-json.test.d.ts.map +1 -0
- package/dist/shadow/consumer-pkg.d.ts +5 -15
- package/dist/shadow/consumer-pkg.d.ts.map +1 -1
- package/dist/shadow/consumer-pkg.js +10 -72
- package/dist/shadow/merge-package-json.d.ts +10 -4
- package/dist/shadow/merge-package-json.d.ts.map +1 -1
- package/dist/shadow/merge-package-json.js +11 -10
- package/dist/shadow/sync.js +2 -2
- package/dist/templates/app/claude-settings.js +13 -0
- package/dist/types.d.ts +0 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/util/__tests__/templates.test.d.ts +2 -0
- package/dist/util/__tests__/templates.test.d.ts.map +1 -0
- package/dist/util/__tests__/workspace-templates-shipped.test.d.ts +2 -0
- package/dist/util/__tests__/workspace-templates-shipped.test.d.ts.map +1 -0
- package/dist/util/__tests__/workspace.test.d.ts +2 -0
- package/dist/util/__tests__/workspace.test.d.ts.map +1 -0
- package/dist/util/s3/upload.js +1 -1
- package/dist/util/templates.d.ts +22 -0
- package/dist/util/templates.d.ts.map +1 -0
- package/dist/util/templates.js +45 -0
- package/dist/util/workspace.d.ts +20 -0
- package/dist/util/workspace.d.ts.map +1 -0
- package/dist/util/workspace.js +57 -0
- package/package.json +5 -3
- package/src/commands/__tests__/create-workspace.test.ts +78 -0
- package/src/commands/__tests__/create.test.ts +111 -0
- package/src/commands/create-workspace.ts +85 -0
- package/src/commands/create.ts +113 -0
- package/src/constants.ts +11 -34
- package/src/index.ts +28 -0
- package/src/shadow/__tests__/consumer-pkg.test.ts +56 -0
- package/src/shadow/__tests__/merge-package-json.test.ts +44 -0
- package/src/shadow/consumer-pkg.ts +10 -88
- package/src/shadow/merge-package-json.ts +12 -11
- package/src/shadow/sync.ts +2 -2
- package/src/templates/app/CLAUDE.md +72 -0
- package/src/templates/app/claude-settings.json +7 -0
- package/src/templates/app/default-index.tsx.tmpl +21 -0
- package/src/templates/app/gitignore +4 -0
- package/src/templates/app/html-index.tsx.tmpl +21 -0
- package/src/templates/app/package.json.tmpl +13 -0
- package/src/templates/workspace/README.md +39 -0
- package/src/templates/workspace/gitignore +23 -0
- package/src/templates/workspace/npmrc +3 -0
- package/src/templates/workspace/nvmrc +1 -0
- package/src/templates/workspace/package.json.tmpl +23 -0
- package/src/templates/workspace/yarn-releases/yarn-4.5.1.cjs +934 -0
- package/src/templates/workspace/yarnrc.yml +7 -0
- package/src/types.ts +4 -1
- package/src/util/__tests__/templates.test.ts +27 -0
- package/src/util/__tests__/workspace-templates-shipped.test.ts +35 -0
- package/src/util/__tests__/workspace.test.ts +65 -0
- package/src/util/s3/upload.ts +1 -1
- package/src/util/templates.ts +37 -0
- package/src/util/workspace.ts +52 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { ensureDir, pathExists, readdir, writeFile, copy } from 'fs-extra';
|
|
4
|
+
import { execa } from 'execa';
|
|
5
|
+
import { info, output, setOutputMode } from '../util/logger';
|
|
6
|
+
import { assertWorkspace } from '../util/workspace';
|
|
7
|
+
import { getTemplatesDir, renderTemplate, APP_TEMPLATES, type AppTemplate } from '../util/templates';
|
|
8
|
+
|
|
9
|
+
export interface CreateOptions {
|
|
10
|
+
template?: string;
|
|
11
|
+
htmlFile?: string;
|
|
12
|
+
json?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const APP_NAME_RE = /^[a-z][a-z0-9-]{1,49}$/;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Convert kebab-case to PascalCase. `demo-app` → `DemoApp`.
|
|
19
|
+
*/
|
|
20
|
+
function pascalCase(input: string): string {
|
|
21
|
+
return input
|
|
22
|
+
.split('-')
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
25
|
+
.join('');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns true iff `dir` either does not exist or is an empty directory.
|
|
30
|
+
*/
|
|
31
|
+
async function isDirEmpty(dir: string): Promise<boolean> {
|
|
32
|
+
if (!(await pathExists(dir))) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
const entries = await readdir(dir);
|
|
36
|
+
return entries.length === 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function createCommand(appName: string, opts: CreateOptions): Promise<void> {
|
|
40
|
+
if (opts.json) {
|
|
41
|
+
setOutputMode('json');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Validate workspace.
|
|
45
|
+
const workspace = await assertWorkspace();
|
|
46
|
+
|
|
47
|
+
// Validate app name.
|
|
48
|
+
if (!APP_NAME_RE.test(appName)) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`App name "${appName}" is invalid. Use lowercase kebab-case (a-z, 0-9, -), ` +
|
|
51
|
+
`2-50 chars, starting with a letter.`
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Resolve template.
|
|
56
|
+
// `--html-file` implies `--template html-embed` unless the user already picked one.
|
|
57
|
+
const template: AppTemplate = (opts.template ?? (opts.htmlFile ? 'html-embed' : 'default')) as AppTemplate;
|
|
58
|
+
if (!APP_TEMPLATES.includes(template)) {
|
|
59
|
+
throw new Error(`Unknown template "${template}". Available: ${APP_TEMPLATES.join(', ')}.`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (opts.htmlFile && !(await pathExists(opts.htmlFile))) {
|
|
63
|
+
throw new Error(`HTML file does not exist: ${opts.htmlFile}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Validate target dir.
|
|
67
|
+
const appDir = path.join(workspace, appName);
|
|
68
|
+
if (!(await isDirEmpty(appDir))) {
|
|
69
|
+
throw new Error(`${appDir} is non-empty. Pick a different name or wipe it first.`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Compute substitutions.
|
|
73
|
+
const vars = {
|
|
74
|
+
__APP_NAME__: appName,
|
|
75
|
+
__APP_COMPONENT__: pascalCase(appName),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const appTemplatesDir = path.join(getTemplatesDir(), 'app');
|
|
79
|
+
await ensureDir(path.join(appDir, 'src'));
|
|
80
|
+
await ensureDir(path.join(appDir, '.claude'));
|
|
81
|
+
|
|
82
|
+
// package.json (templated).
|
|
83
|
+
const pkgRaw = await readFile(path.join(appTemplatesDir, 'package.json.tmpl'), 'utf8');
|
|
84
|
+
await writeFile(path.join(appDir, 'package.json'), renderTemplate(pkgRaw, vars));
|
|
85
|
+
|
|
86
|
+
// src/index.tsx (templated; choice driven by template).
|
|
87
|
+
const indexTemplate = template === 'html-embed' ? 'html-index.tsx.tmpl' : 'default-index.tsx.tmpl';
|
|
88
|
+
const indexRaw = await readFile(path.join(appTemplatesDir, indexTemplate), 'utf8');
|
|
89
|
+
await writeFile(path.join(appDir, 'src', 'index.tsx'), renderTemplate(indexRaw, vars));
|
|
90
|
+
|
|
91
|
+
// Copy verbatim files.
|
|
92
|
+
await copy(path.join(appTemplatesDir, 'gitignore'), path.join(appDir, '.gitignore'));
|
|
93
|
+
await copy(path.join(appTemplatesDir, 'claude-settings.json'), path.join(appDir, '.claude', 'settings.json'));
|
|
94
|
+
await copy(path.join(appTemplatesDir, 'CLAUDE.md'), path.join(appDir, 'CLAUDE.md'));
|
|
95
|
+
|
|
96
|
+
// html-embed: also copy the source HTML.
|
|
97
|
+
if (template === 'html-embed' && opts.htmlFile) {
|
|
98
|
+
await copy(opts.htmlFile, path.join(appDir, 'src', 'page.html'));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
info(`installing into workspace at ${workspace}`);
|
|
102
|
+
await execa('yarn', ['install'], {
|
|
103
|
+
cwd: workspace,
|
|
104
|
+
stdio: 'inherit',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
output(`[z2h-cli] app ready at ${appDir}`, {
|
|
108
|
+
ok: true,
|
|
109
|
+
appDir,
|
|
110
|
+
appName,
|
|
111
|
+
template,
|
|
112
|
+
});
|
|
113
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -6,45 +6,22 @@
|
|
|
6
6
|
// ─────────── Shadow dir layout ───────────
|
|
7
7
|
export const SHADOW_DIR_NAME = '.zth';
|
|
8
8
|
|
|
9
|
-
// ───────────
|
|
10
|
-
/**
|
|
11
|
-
* Protected runtime deps that MUST resolve to a single instance across the
|
|
12
|
-
* wrapper (.zth) and the consumer's src. Declared as peerDependencies in
|
|
13
|
-
* the shadow pkg so yarn hoists them from the workspace root — which is
|
|
14
|
-
* why we enforce their presence in the consumer's own package.json.
|
|
15
|
-
*/
|
|
16
|
-
export const PROTECTED_DEPS: Record<string, string> = {
|
|
17
|
-
react: '^18.2.0',
|
|
18
|
-
'react-dom': '^18.2.0',
|
|
19
|
-
'react-error-boundary': '^3.1.3',
|
|
20
|
-
'@mondaydotcomorg/trident-runtime': '^1.24.1',
|
|
21
|
-
'@vibe/core': '^4.0.1',
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Protected dev deps. Same idempotent treatment as PROTECTED_DEPS but live
|
|
26
|
-
* in the consumer's `devDependencies`. These give the LLM-authored consumer
|
|
27
|
-
* code access to monday.com platform types out of the box.
|
|
28
|
-
*/
|
|
29
|
-
export const PROTECTED_DEV_DEPS: Record<string, string> = {
|
|
30
|
-
'@mondaydotcomorg/mf-api-types': '^0.1.1',
|
|
31
|
-
'@mondaydotcomorg/bigbrain-types': '^0.3.3',
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
// ─────────── Shadow pkg template ───────────
|
|
35
|
-
// TODO — Add trident mf-types / bigbrain-types opt-in.
|
|
36
|
-
export const WORKSPACE_DEV_DEPS: Record<string, string> = {
|
|
37
|
-
'@mondaydotcomorg/trident-toolkit': '^2.9.25',
|
|
38
|
-
'@types/react': '^18.0.14',
|
|
39
|
-
'@types/react-dom': '^18.0.5',
|
|
40
|
-
typescript: '^5.7.2',
|
|
41
|
-
};
|
|
42
|
-
|
|
9
|
+
// ─────────── Shadow scripts ───────
|
|
43
10
|
export const SHADOW_SCRIPTS: Record<string, string> = {
|
|
44
11
|
start: 'trident start',
|
|
45
12
|
build: 'trident build',
|
|
46
13
|
};
|
|
47
14
|
|
|
15
|
+
// ─────────── Shadow dev deps ───────
|
|
16
|
+
// Yarn berry only wires a workspace member's `node_modules/.bin/` PATH
|
|
17
|
+
// for packages that member explicitly declares. The shadow's scripts
|
|
18
|
+
// invoke `trident`, so trident-toolkit must be declared here for the
|
|
19
|
+
// bin to resolve. `*` is fine — yarn workspaces resolve the actual
|
|
20
|
+
// version from the workspace root's pinned entry.
|
|
21
|
+
export const SHADOW_DEV_DEPS: Record<string, string> = {
|
|
22
|
+
'@mondaydotcomorg/trident-toolkit': '*',
|
|
23
|
+
};
|
|
24
|
+
|
|
48
25
|
// ─────────── Auto-allocated dev port range ────
|
|
49
26
|
// High enough to dodge common dev defaults (3000 Next, 5173 Vite, 8080).
|
|
50
27
|
// Collisions within the 7k-port pool are rare for POC; if trident can't
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { devCommand } from './commands/dev';
|
|
|
3
3
|
import { buildCommand } from './commands/build';
|
|
4
4
|
import { cleanCommand } from './commands/clean';
|
|
5
5
|
import { deployCommand } from './commands/deploy';
|
|
6
|
+
import { createWorkspaceCommand } from './commands/create-workspace';
|
|
7
|
+
import { createCommand } from './commands/create';
|
|
6
8
|
import { errorLog, fail } from './util/logger';
|
|
7
9
|
|
|
8
10
|
async function main(): Promise<void> {
|
|
@@ -77,6 +79,32 @@ async function main(): Promise<void> {
|
|
|
77
79
|
}
|
|
78
80
|
});
|
|
79
81
|
|
|
82
|
+
program
|
|
83
|
+
.command('create-workspace')
|
|
84
|
+
.description('Bootstrap the ~/zth-projects/ yarn workspace (one-time setup)')
|
|
85
|
+
.option('--json', 'emit structured JSON on stdout')
|
|
86
|
+
.action(async (opts: { json?: boolean }) => {
|
|
87
|
+
try {
|
|
88
|
+
await createWorkspaceCommand(opts);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
fail(`create-workspace failed: ${(err as Error).message}`, { command: 'create-workspace' }, 1);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
program
|
|
95
|
+
.command('create <app-name>')
|
|
96
|
+
.description('Scaffold a new Z2H consumer app inside ~/zth-projects/')
|
|
97
|
+
.option('--template <name>', 'Template name (default | html-embed)')
|
|
98
|
+
.option('--html-file <path>', 'Path to an HTML file to embed (implies --template html-embed)')
|
|
99
|
+
.option('--json', 'emit structured JSON on stdout')
|
|
100
|
+
.action(async (appName: string, opts: { template?: string; htmlFile?: string; json?: boolean }) => {
|
|
101
|
+
try {
|
|
102
|
+
await createCommand(appName, opts);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
fail(`create failed: ${(err as Error).message}`, { command: 'create' }, 1);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
80
108
|
await program.parseAsync(process.argv);
|
|
81
109
|
}
|
|
82
110
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { mkdtemp, rm, writeJson, readJson } from 'fs-extra';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { ensureConsumerPkg } from '../consumer-pkg';
|
|
6
|
+
|
|
7
|
+
describe('ensureConsumerPkg (slimmed)', () => {
|
|
8
|
+
let tmp: string;
|
|
9
|
+
let pkgPath: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
tmp = await mkdtemp(path.join(os.tmpdir(), 'z2h-pkg-'));
|
|
13
|
+
pkgPath = path.join(tmp, 'package.json');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await rm(tmp, { recursive: true, force: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('adds `private: true` when missing', async () => {
|
|
21
|
+
await writeJson(pkgPath, { name: 'demo', version: '0.0.0' });
|
|
22
|
+
const result = await ensureConsumerPkg(pkgPath, { name: 'demo', version: '0.0.0' });
|
|
23
|
+
expect(result.changed).toBe(true);
|
|
24
|
+
expect(result.pkg.private).toBe(true);
|
|
25
|
+
const onDisk = await readJson(pkgPath);
|
|
26
|
+
expect(onDisk.private).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('is a no-op when private:true is already present', async () => {
|
|
30
|
+
const initial = { name: 'demo', version: '0.0.0', private: true };
|
|
31
|
+
await writeJson(pkgPath, initial);
|
|
32
|
+
const result = await ensureConsumerPkg(pkgPath, initial);
|
|
33
|
+
expect(result.changed).toBe(false);
|
|
34
|
+
expect(result.pkg.private).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('does NOT add a `workspaces` field', async () => {
|
|
38
|
+
await writeJson(pkgPath, { name: 'demo' });
|
|
39
|
+
const result = await ensureConsumerPkg(pkgPath, { name: 'demo' });
|
|
40
|
+
expect(result.pkg.workspaces).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('does NOT inject runtime deps that the workspace root owns', async () => {
|
|
44
|
+
await writeJson(pkgPath, { name: 'demo' });
|
|
45
|
+
const result = await ensureConsumerPkg(pkgPath, { name: 'demo' });
|
|
46
|
+
expect(result.pkg.dependencies).toBeUndefined();
|
|
47
|
+
expect(result.pkg.devDependencies).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('preserves existing dependencies declared by the consumer', async () => {
|
|
51
|
+
const initial = { name: 'demo', dependencies: { lodash: '^4.0.0' } };
|
|
52
|
+
await writeJson(pkgPath, initial);
|
|
53
|
+
const result = await ensureConsumerPkg(pkgPath, initial);
|
|
54
|
+
expect(result.pkg.dependencies).toEqual({ lodash: '^4.0.0' });
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildShadowPkg, hashConsumerDeps } from '../merge-package-json';
|
|
3
|
+
|
|
4
|
+
describe('buildShadowPkg', () => {
|
|
5
|
+
it('emits the @trident scope for the name', () => {
|
|
6
|
+
const shadow = buildShadowPkg({ name: 'demo-app' });
|
|
7
|
+
expect(shadow.name).toMatch(/^@trident\//);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it('marks the shadow private', () => {
|
|
11
|
+
const shadow = buildShadowPkg({ name: 'demo' });
|
|
12
|
+
expect(shadow.private).toBe(true);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('declares only trident-toolkit so the trident bin is on the shadow PATH', () => {
|
|
16
|
+
const shadow = buildShadowPkg({ name: 'demo' });
|
|
17
|
+
expect(shadow.devDependencies).toEqual({ '@mondaydotcomorg/trident-toolkit': '*' });
|
|
18
|
+
const asAny = shadow as unknown as Record<string, unknown>;
|
|
19
|
+
expect(asAny.dependencies).toBeUndefined();
|
|
20
|
+
expect(asAny.peerDependencies).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('uses plain trident scripts (no `-T`); bin resolves via the local devDep', () => {
|
|
24
|
+
const shadow = buildShadowPkg({ name: 'demo' });
|
|
25
|
+
expect(shadow.scripts).toEqual({
|
|
26
|
+
start: 'trident start',
|
|
27
|
+
build: 'trident build',
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('hashConsumerDeps', () => {
|
|
33
|
+
it('returns the same hash for equivalent dep maps regardless of key order', () => {
|
|
34
|
+
const a = hashConsumerDeps({ name: 'x', dependencies: { b: '1', a: '2' } });
|
|
35
|
+
const b = hashConsumerDeps({ name: 'x', dependencies: { a: '2', b: '1' } });
|
|
36
|
+
expect(a).toBe(b);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns different hashes when deps change', () => {
|
|
40
|
+
const a = hashConsumerDeps({ name: 'x', dependencies: { a: '1' } });
|
|
41
|
+
const b = hashConsumerDeps({ name: 'x', dependencies: { a: '2' } });
|
|
42
|
+
expect(a).not.toBe(b);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -1,100 +1,22 @@
|
|
|
1
1
|
import { writeJson } from 'fs-extra';
|
|
2
|
-
import { PROTECTED_DEPS, PROTECTED_DEV_DEPS, SHADOW_DIR_NAME } from '../constants';
|
|
3
2
|
import type { ConsumerPkg } from '../types';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*/
|
|
10
|
-
export function ensureConsumerWorkspaces(pkg: ConsumerPkg): {
|
|
11
|
-
changed: boolean;
|
|
12
|
-
workspaces: ConsumerPkg['workspaces'];
|
|
13
|
-
} {
|
|
14
|
-
const { workspaces } = pkg;
|
|
15
|
-
|
|
16
|
-
if (Array.isArray(workspaces)) {
|
|
17
|
-
if (workspaces.includes(SHADOW_DIR_NAME)) {
|
|
18
|
-
return { changed: false, workspaces };
|
|
19
|
-
}
|
|
20
|
-
return { changed: true, workspaces: [...workspaces, SHADOW_DIR_NAME] };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (workspaces && typeof workspaces === 'object') {
|
|
24
|
-
const pkgs = workspaces.packages ?? [];
|
|
25
|
-
if (pkgs.includes(SHADOW_DIR_NAME)) {
|
|
26
|
-
return { changed: false, workspaces };
|
|
27
|
-
}
|
|
28
|
-
return {
|
|
29
|
-
changed: true,
|
|
30
|
-
workspaces: { ...workspaces, packages: [...pkgs, SHADOW_DIR_NAME] },
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return { changed: true, workspaces: [SHADOW_DIR_NAME] };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Extend a deps map with the protected entries; only fills in missing ones,
|
|
39
|
-
* never overrides versions the consumer already pinned.
|
|
40
|
-
*/
|
|
41
|
-
function applyProtected(
|
|
42
|
-
current: Record<string, string> | undefined,
|
|
43
|
-
protectedMap: Record<string, string>
|
|
44
|
-
): { changed: boolean; next: Record<string, string> } {
|
|
45
|
-
const next = { ...(current ?? {}) };
|
|
46
|
-
let changed = false;
|
|
47
|
-
for (const [name, version] of Object.entries(protectedMap)) {
|
|
48
|
-
if (!next[name]) {
|
|
49
|
-
next[name] = version;
|
|
50
|
-
changed = true;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return { changed, next };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Idempotently reshape the consumer's package.json so that:
|
|
58
|
-
* - it is marked `"private": true`
|
|
59
|
-
* - `.zth` is declared as a yarn workspace member (yarn will hoist)
|
|
60
|
-
* - the protected runtime deps are present at compatible versions
|
|
61
|
-
* - the protected dev deps are present at compatible versions
|
|
5
|
+
* The consumer pkg is a thin manifest. Z2H apps live inside a yarn
|
|
6
|
+
* workspace at `~/zth-projects/`; runtime and dev deps come from the
|
|
7
|
+
* workspace root, hoisted into a single `node_modules/`.
|
|
62
8
|
*
|
|
63
|
-
*
|
|
9
|
+
* The only thing this file enforces today is `private: true` — apps must
|
|
10
|
+
* never accidentally publish to npm.
|
|
64
11
|
*/
|
|
65
12
|
export async function ensureConsumerPkg(
|
|
66
13
|
pkgPath: string,
|
|
67
14
|
pkg: ConsumerPkg
|
|
68
15
|
): Promise<{ changed: boolean; pkg: ConsumerPkg }> {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (next.private !== true) {
|
|
73
|
-
next.private = true;
|
|
74
|
-
changed = true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const ws = ensureConsumerWorkspaces(next);
|
|
78
|
-
if (ws.changed) {
|
|
79
|
-
next.workspaces = ws.workspaces;
|
|
80
|
-
changed = true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const deps = applyProtected(next.dependencies, PROTECTED_DEPS);
|
|
84
|
-
if (deps.changed) {
|
|
85
|
-
next.dependencies = deps.next;
|
|
86
|
-
changed = true;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const devDeps = applyProtected(next.devDependencies, PROTECTED_DEV_DEPS);
|
|
90
|
-
if (devDeps.changed) {
|
|
91
|
-
next.devDependencies = devDeps.next;
|
|
92
|
-
changed = true;
|
|
16
|
+
if (pkg.private === true) {
|
|
17
|
+
return { changed: false, pkg };
|
|
93
18
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return { changed, pkg: next };
|
|
19
|
+
const next: ConsumerPkg = { ...pkg, private: true };
|
|
20
|
+
await writeJson(pkgPath, next, { spaces: 2 });
|
|
21
|
+
return { changed: true, pkg: next };
|
|
100
22
|
}
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import { writeJson } from 'fs-extra';
|
|
3
|
-
import {
|
|
3
|
+
import { SHADOW_SCRIPTS, SHADOW_DEV_DEPS } from '../constants';
|
|
4
4
|
import { mfAppName } from '../util/app-name';
|
|
5
5
|
import type { ConsumerPkg, ShadowPkg } from '../types';
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Build the shadow package.json.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* Build the shadow package.json.
|
|
9
|
+
*
|
|
10
|
+
* Runtime deps live at the workspace root (`~/zth-projects/`) and are
|
|
11
|
+
* hoisted in via yarn workspaces — no need to mirror them here. The shadow
|
|
12
|
+
* only declares the toolchain it directly executes (`trident-toolkit`) so
|
|
13
|
+
* yarn berry wires the `trident` bin into the shadow's script PATH.
|
|
14
|
+
*
|
|
15
|
+
* The `@trident/` scope is preserved so trident-toolkit strips it and the
|
|
16
|
+
* emitted bundle becomes `MF-mf-<appName>`, matching the URL the host MF
|
|
17
|
+
* routes to.
|
|
12
18
|
*/
|
|
13
19
|
export function buildShadowPkg(consumerPkg: ConsumerPkg): ShadowPkg {
|
|
14
|
-
// Use the `@trident/` scope so trident-toolkit strips it and the emitted
|
|
15
|
-
// bundle becomes `MF-mf-<appName>`, matching the URL the host MF routes to.
|
|
16
|
-
// The scope also keeps the shadow's name distinct from the consumer's
|
|
17
|
-
// package.json name (same workspace, different identifiers).
|
|
18
20
|
return {
|
|
19
21
|
name: `@trident/${mfAppName(consumerPkg.name)}`,
|
|
20
22
|
version: '0.0.1',
|
|
21
23
|
private: true,
|
|
22
24
|
scripts: SHADOW_SCRIPTS,
|
|
23
|
-
|
|
24
|
-
devDependencies: WORKSPACE_DEV_DEPS,
|
|
25
|
+
devDependencies: { ...SHADOW_DEV_DEPS },
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
|
package/src/shadow/sync.ts
CHANGED
|
@@ -19,8 +19,8 @@ export async function syncShadow(paths: ConsumerPaths, consumerPkg: ConsumerPkg)
|
|
|
19
19
|
|
|
20
20
|
if (oldHash !== newHash) {
|
|
21
21
|
info(oldHash ? 'consumer deps changed — yarn install' : 'first run — yarn install');
|
|
22
|
-
// Yarn 4 is cache-first and non-interactive by default
|
|
23
|
-
//
|
|
22
|
+
// Yarn 4 is cache-first and non-interactive by default in non-TTY
|
|
23
|
+
// contexts, so plain `yarn install` is all we need.
|
|
24
24
|
await execa('yarn', ['install'], {
|
|
25
25
|
cwd: paths.consumerDir,
|
|
26
26
|
stdio: 'inherit',
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# This Z2H Consumer App — Claude Instructions
|
|
2
|
+
|
|
3
|
+
You are working inside a **Z2H consumer app** — a standalone React app that ships to `https://bigbrain.me/bigbrain-vibe/<app>` via the `z2h` plugin.
|
|
4
|
+
|
|
5
|
+
**You are Z2H, not a generic developer assistant.** The user is non-technical. Speak in product outcomes ("your search now filters as you type"), not code mechanics ("I added a useState hook").
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ⛔ Code Change Interception
|
|
10
|
+
|
|
11
|
+
Before reading or editing ANY source file, if the user says anything like:
|
|
12
|
+
|
|
13
|
+
- "fix", "change", "broken", "wrong", "improve", "add", "make it…"
|
|
14
|
+
- "deploy", "ship it", "push to production"
|
|
15
|
+
- Any product feedback ("the search is slow", "the table looks off")
|
|
16
|
+
|
|
17
|
+
**→ Invoke `z2h:zero-to-hero` FIRST.** Do not touch files directly. **Even one-line fixes and typos go through the orchestrator** — "it's just a small change" is exactly the case where direct edits silently skip deploy and production stays broken.
|
|
18
|
+
|
|
19
|
+
The orchestrator routes to `z2h:feature-development`, which handles the typecheck gate, opens a live preview, asks you to confirm, then publishes via `z2h-cli deploy`.
|
|
20
|
+
|
|
21
|
+
**The only exceptions** (read-only or already inside the workflow):
|
|
22
|
+
|
|
23
|
+
- "Explain this code" / "What does this do" — pure read, no edits
|
|
24
|
+
- You're already executing inside `z2h:feature-development`
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 🌟 Prefer Frontend Over Backend
|
|
29
|
+
|
|
30
|
+
If a request *could* be solved in this app using existing backend endpoints — do that. Backend changes go through a slow PR + Harmony pipeline; frontend changes ship in seconds via `z2h-cli deploy`.
|
|
31
|
+
|
|
32
|
+
When uncertain, ask:
|
|
33
|
+
|
|
34
|
+
> "Can we do this in the app with existing endpoints, or does it need a backend change?"
|
|
35
|
+
|
|
36
|
+
Only escalate to `z2h:backend-feature-development` for genuinely new persisted entities, schemas, write paths, or server-side rules.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Architecture Facts (about this folder)
|
|
41
|
+
|
|
42
|
+
- This folder is **standalone** — not part of any monorepo, no git initialized.
|
|
43
|
+
- `src/index.tsx` default-exports the React component the host MF loads.
|
|
44
|
+
- `package.json` wires the `z2h-cli` for `build` / `start` / `deploy`.
|
|
45
|
+
- `.zth/` is an auto-generated shadow workspace — never edit it by hand; `npx z2h-cli clean` wipes it.
|
|
46
|
+
- Local dev: `npx z2h-cli dev` → opens at `https://bigbrain.me/bigbrain-vibe/<app>?zth_local=<port>`.
|
|
47
|
+
- Production: `npx z2h-cli deploy` → live at `https://bigbrain.me/bigbrain-vibe/<app>`.
|
|
48
|
+
- There is no git, no PR, no CI. The gate is: lint pass → typecheck pass → user previews and approves.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## ⛔ No Agent Visual Testing
|
|
53
|
+
|
|
54
|
+
When you make a change, **do not screenshot, do not run `axcli chrome`, do not run `playwright-cli`, do not open browser tabs**. The user is sitting in front of the live preview and verifies with their own eyes — HMR updates instantly. Edit → tell the user → wait for their approval. Agent-side visual checks have proven slow and unreliable in this flow.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Tone (mandatory)
|
|
59
|
+
|
|
60
|
+
Banned in user-facing output: PR, branch, merge, CI, deploy (as noun), S3, manifest, lint, ESLint, TypeScript, repo, monorepo, microfrontend, MF, yarn, npm, tsx, jsx, scss, commit, push.
|
|
61
|
+
|
|
62
|
+
Use instead: "changes", "code quality check", "publish", "go live", "your app".
|
|
63
|
+
|
|
64
|
+
Every step transition includes a time estimate. Transition pattern: what just finished → what's next → how long.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Where the Skills Live
|
|
69
|
+
|
|
70
|
+
The Z2H plugin is installed globally — every Claude session has access to `z2h:zero-to-hero`, `z2h:feature-development`, `z2h:run-mf-locally`, `z2h:plan`, etc. **Always go through `z2h:zero-to-hero`** for user requests; never invoke sub-skills directly.
|
|
71
|
+
|
|
72
|
+
If `z2h:zero-to-hero` is not available in this session, the plugin isn't installed in the user's Claude config. Direct them to: `claude plugin marketplace add <bigbrain-z2h-path>` then `claude plugin install z2h`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Always import the default React alongside any named hooks. Trident's MFE
|
|
2
|
+
// federation initializes the shared `react` module via the default-import
|
|
3
|
+
// code path; pure named-only imports resolve to null at runtime.
|
|
4
|
+
import React, { useState } from 'react';
|
|
5
|
+
|
|
6
|
+
export default function __APP_COMPONENT__() {
|
|
7
|
+
const [count, setCount] = useState(0);
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div style={{ width: '100vw', padding: 24, fontFamily: 'system-ui, sans-serif' }}>
|
|
11
|
+
<h1>Hello from Z2H 👋</h1>
|
|
12
|
+
<p>Edit <code>src/index.tsx</code> and the page will hot-reload.</p>
|
|
13
|
+
<button
|
|
14
|
+
onClick={() => setCount(c => c + 1)}
|
|
15
|
+
style={{ padding: '8px 16px', fontSize: 16, cursor: 'pointer' }}
|
|
16
|
+
>
|
|
17
|
+
Clicked {count} times
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// HTML-embed entrypoint. Renders src/page.html in an iframe, with the file
|
|
2
|
+
// imported via Vite's `?url` suffix so Vite's dev server hosts it at
|
|
3
|
+
// /src/page.html (and copies it to /assets/ at build time). Same-origin
|
|
4
|
+
// (the URL lives under whatever host is rendering the app), so the page's
|
|
5
|
+
// fetch() calls carry bigbrain.me auth cookies normally.
|
|
6
|
+
//
|
|
7
|
+
// The iframe gives the page its own document — scripts execute natively,
|
|
8
|
+
// CSS/JS is fully isolated from the host React app, no script reinjection
|
|
9
|
+
// logic needed.
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import pageUrl from './page.html?url';
|
|
12
|
+
|
|
13
|
+
export default function __APP_COMPONENT__() {
|
|
14
|
+
return (
|
|
15
|
+
<iframe
|
|
16
|
+
src={pageUrl}
|
|
17
|
+
title="App"
|
|
18
|
+
style={{ width: '100vw', height: '100vh', border: 'none', display: 'block' }}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Z2H Projects Workspace
|
|
2
|
+
|
|
3
|
+
This folder hosts every Z2H consumer app you build. Each subfolder is a standalone
|
|
4
|
+
React app that ships to `https://bigbrain.me/bigbrain-vibe/<app>`.
|
|
5
|
+
|
|
6
|
+
## How it works
|
|
7
|
+
|
|
8
|
+
- This folder is a **yarn workspace**. All apps share one `node_modules/` and one
|
|
9
|
+
`yarn.lock` — installs are fast, deps stay consistent across apps.
|
|
10
|
+
- Each app folder is a workspace member; the per-app `.zth/` folder (auto-generated
|
|
11
|
+
by the Z2H CLI) is also a workspace member, kept in sync with the CLI version.
|
|
12
|
+
- Do not edit `node_modules/`, `yarn.lock`, or any `.zth/` folder by hand.
|
|
13
|
+
|
|
14
|
+
## Add an app
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
z2h-cli create <app-name>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Templates: `default` (React entry, the usual case) and `html-embed` (drops an
|
|
21
|
+
existing `.html` file into an iframe). Add `--template html-embed --html-file <path>`
|
|
22
|
+
for the second flow.
|
|
23
|
+
|
|
24
|
+
## Run / deploy an app
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cd <app-name>
|
|
28
|
+
z2h-cli dev # local preview at https://bigbrain.me/bigbrain-vibe/<app>?zth_local=<port>
|
|
29
|
+
z2h-cli deploy # publishes to S3, live at https://bigbrain.me/bigbrain-vibe/<app>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Reset
|
|
33
|
+
|
|
34
|
+
If you need a clean slate:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
rm -rf ~/zth-projects
|
|
38
|
+
z2h-cli create-workspace
|
|
39
|
+
```
|