@mondaydotcomorg/z2h-cli 0.4.1 → 0.5.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.
Files changed (47) hide show
  1. package/dist/commands/create-workspace.d.ts.map +1 -1
  2. package/dist/commands/create-workspace.js +3 -30
  3. package/dist/commands/migrate.d.ts +6 -0
  4. package/dist/commands/migrate.d.ts.map +1 -0
  5. package/dist/commands/migrate.js +13 -0
  6. package/dist/esm/commands/create-workspace.d.ts.map +1 -1
  7. package/dist/esm/commands/create-workspace.mjs +5 -32
  8. package/dist/esm/commands/migrate.d.ts +6 -0
  9. package/dist/esm/commands/migrate.d.ts.map +1 -0
  10. package/dist/esm/commands/migrate.mjs +11 -0
  11. package/dist/esm/index.mjs +14 -0
  12. package/dist/esm/migrations/001-remove-per-consumer-yarnrc.d.ts +3 -0
  13. package/dist/esm/migrations/001-remove-per-consumer-yarnrc.d.ts.map +1 -0
  14. package/dist/esm/migrations/001-remove-per-consumer-yarnrc.mjs +15 -0
  15. package/dist/esm/migrations/002-remove-z2h-cli-from-workspace-deps.d.ts +3 -0
  16. package/dist/esm/migrations/002-remove-z2h-cli-from-workspace-deps.d.ts.map +1 -0
  17. package/dist/esm/migrations/002-remove-z2h-cli-from-workspace-deps.mjs +41 -0
  18. package/dist/esm/migrations/auto-migrate.d.ts +2 -0
  19. package/dist/esm/migrations/auto-migrate.d.ts.map +1 -0
  20. package/dist/esm/migrations/auto-migrate.mjs +166 -0
  21. package/dist/esm/templates/workspace/nx.mjs +5 -0
  22. package/dist/index.js +14 -0
  23. package/dist/migrations/001-remove-per-consumer-yarnrc.d.ts +3 -0
  24. package/dist/migrations/001-remove-per-consumer-yarnrc.d.ts.map +1 -0
  25. package/dist/migrations/001-remove-per-consumer-yarnrc.js +17 -0
  26. package/dist/migrations/002-remove-z2h-cli-from-workspace-deps.d.ts +3 -0
  27. package/dist/migrations/002-remove-z2h-cli-from-workspace-deps.d.ts.map +1 -0
  28. package/dist/migrations/002-remove-z2h-cli-from-workspace-deps.js +43 -0
  29. package/dist/migrations/auto-migrate.d.ts +2 -0
  30. package/dist/migrations/auto-migrate.d.ts.map +1 -0
  31. package/dist/migrations/auto-migrate.js +174 -0
  32. package/dist/templates/workspace/nx.js +7 -0
  33. package/migrations.json +14 -0
  34. package/package.json +9 -2
  35. package/src/commands/__tests__/create-workspace.test.ts +2 -4
  36. package/src/commands/__tests__/create.test.ts +0 -1
  37. package/src/commands/create-workspace.ts +5 -33
  38. package/src/commands/migrate.ts +15 -0
  39. package/src/index.ts +14 -0
  40. package/src/migrations/001-remove-per-consumer-yarnrc.ts +15 -0
  41. package/src/migrations/002-remove-z2h-cli-from-workspace-deps.ts +47 -0
  42. package/src/migrations/auto-migrate.ts +214 -0
  43. package/src/templates/app/CLAUDE.md +1 -1
  44. package/src/templates/app/package.json.tmpl +1 -4
  45. package/src/templates/workspace/nx.json +3 -0
  46. package/src/templates/workspace/package.json.tmpl +4 -1
  47. package/src/util/__tests__/workspace-templates-shipped.test.ts +1 -0
@@ -0,0 +1,174 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ const path = require('node:path');
4
+ const node_module = require('node:module');
5
+ const node_url = require('node:url');
6
+ const fsExtra = require('fs-extra');
7
+ const execa = require('execa');
8
+ const semver = require('semver');
9
+ const util_logger = require('../util/logger.js');
10
+ const util_workspace = require('../util/workspace.js');
11
+
12
+ var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
13
+ const _interopDefault = e => e && e.__esModule ? e : { default: e };
14
+
15
+ const path__default = /*#__PURE__*/_interopDefault(path);
16
+ const semver__default = /*#__PURE__*/_interopDefault(semver);
17
+
18
+ const STATE_FILE = '.z2h-migrations-state.json';
19
+ const NX_JSON_CONTENT = '{\n "extends": "@mondaydotcomorg/trident-monorepo/presets/nx.json"\n}\n';
20
+ // ─── Helpers ───
21
+ async function readState(workspaceHome) {
22
+ const statePath = path__default.default.join(workspaceHome, STATE_FILE);
23
+ if (!(await fsExtra.pathExists(statePath))) {
24
+ return null;
25
+ }
26
+ try {
27
+ return (await fsExtra.readJson(statePath));
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ async function writeState(workspaceHome, state) {
34
+ const statePath = path__default.default.join(workspaceHome, STATE_FILE);
35
+ await fsExtra.writeJson(statePath, state, { spaces: 2 });
36
+ }
37
+ async function yarnInstall(workspaceHome, context) {
38
+ util_logger.info(`${context} — running yarn install...`);
39
+ try {
40
+ await execa.execa('yarn', ['install'], { cwd: workspaceHome, stdio: 'pipe' });
41
+ return true;
42
+ }
43
+ catch (err) {
44
+ const stderr = err.stderr || err.message;
45
+ util_logger.warn(`yarn install failed: ${stderr}`);
46
+ return false;
47
+ }
48
+ }
49
+ async function getLatestTrientMonorepoVersion() {
50
+ try {
51
+ const { stdout } = await execa.execa('npm', ['view', '@mondaydotcomorg/trident-monorepo', 'version']);
52
+ return `^${stdout.trim()}`;
53
+ }
54
+ catch {
55
+ return '*';
56
+ }
57
+ }
58
+ // ─── Workspace bootstrap ───
59
+ function isNxAvailable(workspaceHome) {
60
+ try {
61
+ const wsRequire = node_module.createRequire(path__default.default.join(workspaceHome, 'package.json'));
62
+ wsRequire.resolve('nx/src/generators/tree');
63
+ return true;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ async function ensureWorkspaceNx(workspaceHome) {
70
+ const nxJsonPath = path__default.default.join(workspaceHome, 'nx.json');
71
+ const pkgPath = path__default.default.join(workspaceHome, 'package.json');
72
+ if (!(await fsExtra.pathExists(nxJsonPath))) {
73
+ await fsExtra.writeFile(nxJsonPath, NX_JSON_CONTENT);
74
+ util_logger.info('added nx.json to workspace');
75
+ }
76
+ const pkg = (await fsExtra.readJson(pkgPath));
77
+ if (!pkg.devDependencies?.['@mondaydotcomorg/trident-monorepo']) {
78
+ pkg.devDependencies = pkg.devDependencies ?? {};
79
+ pkg.devDependencies['@mondaydotcomorg/trident-monorepo'] = await getLatestTrientMonorepoVersion();
80
+ }
81
+ if (!pkg.scripts?.nx) {
82
+ pkg.scripts = pkg.scripts ?? {};
83
+ pkg.scripts.nx = 'nx';
84
+ }
85
+ await fsExtra.writeJson(pkgPath, pkg, { spaces: 2 });
86
+ // Only install if NX isn't already importable from the workspace
87
+ if (!isNxAvailable(workspaceHome)) {
88
+ const ok = await yarnInstall(workspaceHome, 'NX not found, installing');
89
+ if (!ok) {
90
+ util_logger.warn('workspace migrations require NX. Fix the install error and run "z2h-cli migrate" to retry.');
91
+ return false;
92
+ }
93
+ }
94
+ return true;
95
+ }
96
+ // ─── Main ───
97
+ async function autoMigrate(cliVersion, dryRun = false) {
98
+ const workspaceHome = util_workspace.getWorkspaceHome();
99
+ if (!(await util_workspace.isZ2hWorkspace(workspaceHome))) {
100
+ util_logger.warn('no Z2H workspace found');
101
+ return;
102
+ }
103
+ const state = await readState(workspaceHome);
104
+ const lastVersion = state?.lastMigratedVersion ?? '0.0.0';
105
+ if (!semver__default.default.gt(cliVersion, lastVersion)) {
106
+ util_logger.info('workspace is up to date — no migrations pending');
107
+ return;
108
+ }
109
+ util_logger.info(`migrating workspace from v${lastVersion} → v${cliVersion}...`);
110
+ // migrations.json is at the package root — two dirs up from dist/migrations/
111
+ const thisDir = path__default.default.dirname(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('migrations/auto-migrate.js', document.baseURI).href))));
112
+ const migrationsJsonPath = path__default.default.resolve(thisDir, '..', '..', 'migrations.json');
113
+ if (!(await fsExtra.pathExists(migrationsJsonPath))) {
114
+ util_logger.warn('migrations.json not found — skipping migrations');
115
+ return;
116
+ }
117
+ const migrationsJson = (await fsExtra.readJson(migrationsJsonPath));
118
+ // Filter to pending migrations (version > lastVersion AND <= cliVersion)
119
+ const pending = Object.entries(migrationsJson.migrations)
120
+ .filter(([, entry]) => semver__default.default.gt(entry.version, lastVersion) && semver__default.default.lte(entry.version, cliVersion))
121
+ .sort(([, a], [, b]) => semver__default.default.compare(a.version, b.version));
122
+ if (dryRun) {
123
+ util_logger.info(`${pending.length} pending migration(s) (dry-run — no changes will be applied):`);
124
+ for (const [, entry] of pending) {
125
+ util_logger.info(` - ${entry.description} (v${entry.version})`);
126
+ }
127
+ return;
128
+ }
129
+ if (pending.length === 0) {
130
+ await writeState(workspaceHome, { lastMigratedVersion: cliVersion });
131
+ return;
132
+ }
133
+ const nxReady = await ensureWorkspaceNx(workspaceHome);
134
+ if (!nxReady) {
135
+ return;
136
+ }
137
+ util_logger.info(`applying ${pending.length} workspace migration(s)...`);
138
+ // Resolve NX from the workspace's node_modules (not from CLI's own deps)
139
+ const wsRequire = node_module.createRequire(path__default.default.join(workspaceHome, 'package.json'));
140
+ const { FsTree, flushChanges } = wsRequire('nx/src/generators/tree');
141
+ let needsInstall = false;
142
+ for (const [name, entry] of pending) {
143
+ const tree = new FsTree(workspaceHome, false);
144
+ try {
145
+ const factoryPath = path__default.default.resolve(path__default.default.dirname(migrationsJsonPath), entry.migrate);
146
+ const mod = require(factoryPath);
147
+ const fn = mod.default || mod;
148
+ fn(tree);
149
+ const changes = tree.listChanges();
150
+ if (changes.length > 0) {
151
+ if (changes.some(c => c.path.endsWith('package.json'))) {
152
+ needsInstall = true;
153
+ }
154
+ flushChanges(workspaceHome, changes);
155
+ util_logger.info(` ✓ ${entry.description}`);
156
+ }
157
+ else {
158
+ util_logger.info(` ○ ${entry.description} (no changes needed)`);
159
+ }
160
+ }
161
+ catch (err) {
162
+ util_logger.warn(` ✗ migration "${name}" failed: ${err.message}`);
163
+ util_logger.warn(' skipping remaining migrations. Run "z2h-cli migrate" to retry.');
164
+ return;
165
+ }
166
+ }
167
+ if (needsInstall) {
168
+ await yarnInstall(workspaceHome, 'package.json changed');
169
+ }
170
+ await writeState(workspaceHome, { lastMigratedVersion: cliVersion });
171
+ util_logger.info('workspace migrations complete.');
172
+ }
173
+
174
+ exports.autoMigrate = autoMigrate;
@@ -0,0 +1,7 @@
1
+ Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: 'Module' } });
2
+
3
+ const nx = {
4
+ "extends": "@mondaydotcomorg/trident-monorepo/presets/nx.json"
5
+ };
6
+
7
+ exports.default = nx;
@@ -0,0 +1,14 @@
1
+ {
2
+ "migrations": {
3
+ "remove-per-consumer-yarnrc": {
4
+ "version": "0.4.1",
5
+ "description": "Remove per-consumer .yarnrc.yml files (workspace root provides config)",
6
+ "migrate": "./dist/migrations/001-remove-per-consumer-yarnrc"
7
+ },
8
+ "remove-z2h-cli-from-workspace-deps": {
9
+ "version": "0.4.1",
10
+ "description": "Remove z2h-cli from workspace and consumer devDependencies (managed globally)",
11
+ "migrate": "./dist/migrations/002-remove-z2h-cli-from-workspace-deps"
12
+ }
13
+ }
14
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mondaydotcomorg/z2h-cli",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "bin": "./bin/z2h-cli.js",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -34,7 +34,8 @@
34
34
  "files": [
35
35
  "bin/",
36
36
  "dist/",
37
- "src/"
37
+ "src/",
38
+ "migrations.json"
38
39
  ],
39
40
  "dependencies": {
40
41
  "@aws-sdk/client-s3": "^3.706.0",
@@ -47,18 +48,24 @@
47
48
  "mime-types": "^2.1.35",
48
49
  "p-map": "^7.0.2",
49
50
  "picocolors": "^1.1.1",
51
+ "semver": "^7.6.0",
50
52
  "tsx": "^4.19.2"
51
53
  },
52
54
  "devDependencies": {
53
55
  "@mondaydotcomorg/trident-library": "^1.2.7",
56
+ "@nx/devkit": "^21.6.0",
54
57
  "@types/cors": "^2.8.19",
55
58
  "@types/express": "^5.0.6",
56
59
  "@types/fs-extra": "^11.0.4",
57
60
  "@types/mime-types": "^2.1.4",
58
61
  "@types/node": "^22.19.0",
62
+ "@types/semver": "^7.5.0",
59
63
  "typescript": "^5.7.2",
60
64
  "vitest": "^2.1.4"
61
65
  },
66
+ "engines": {
67
+ "node": ">=24"
68
+ },
62
69
  "repository": {
63
70
  "type": "git",
64
71
  "url": "https://github.com/DaPulse/bigbrain-z2h.git",
@@ -61,13 +61,11 @@ describe('create-workspace', () => {
61
61
  expect(execa).not.toHaveBeenCalled();
62
62
  });
63
63
 
64
- it('substitutes the current z2h-cli version into the workspace template', async () => {
64
+ it('does not include z2h-cli in workspace devDeps (managed globally)', async () => {
65
65
  await createWorkspaceCommand({});
66
66
 
67
67
  const pkg = await readJson(path.join(tmp, 'package.json'));
68
- const cliDep = pkg.devDependencies['@mondaydotcomorg/z2h-cli'];
69
- expect(cliDep).toBeTruthy();
70
- expect(cliDep).not.toBe('__Z2H_CLI_VERSION__'); // should be substituted
68
+ expect(pkg.devDependencies['@mondaydotcomorg/z2h-cli']).toBeUndefined();
71
69
  });
72
70
 
73
71
  it('ships the yarn berry release inline so any `yarn` invocation runs 4.x without corepack', async () => {
@@ -70,7 +70,6 @@ describe('create', () => {
70
70
  const pkg = await readJson(path.join(appDir, 'package.json'));
71
71
  expect(pkg.name).toBe('demo-app');
72
72
  expect(pkg.private).toBe(true);
73
- expect(pkg.scripts.start).toBe('z2h-cli dev');
74
73
 
75
74
  const indexSrc = (await import('node:fs/promises')).readFile;
76
75
  const content = await indexSrc(path.join(appDir, 'src', 'index.tsx'), 'utf8');
@@ -1,30 +1,15 @@
1
1
  import path from 'node:path';
2
2
  import { readFile } from 'node:fs/promises';
3
- import { ensureDir, readJson, writeFile, pathExists, readdir, copy } from 'fs-extra';
3
+ import { ensureDir, writeFile, pathExists, readdir, copy } from 'fs-extra';
4
4
  import { execa } from 'execa';
5
5
  import { info, output, setOutputMode } from '../util/logger';
6
6
  import { getWorkspaceHome } from '../util/workspace';
7
- import { getTemplatesDir, renderTemplate } from '../util/templates';
7
+ import { getTemplatesDir } from '../util/templates';
8
8
 
9
9
  export interface CreateWorkspaceOptions {
10
10
  json?: boolean;
11
11
  }
12
12
 
13
- /**
14
- * Read the current z2h-cli version from its own package.json — the version
15
- * that gets pinned into the workspace's devDependencies. That way the
16
- * workspace's `@mondaydotcomorg/z2h-cli` always matches the version that
17
- * bootstrapped it.
18
- */
19
- async function readCurrentCliVersion(): Promise<string> {
20
- // util/templates.ts already locates `<package-root>/src/templates`. The
21
- // package's own package.json sits two levels above templates/.
22
- const templatesDir = getTemplatesDir();
23
- const pkgPath = path.resolve(templatesDir, '..', '..', 'package.json');
24
- const pkg = (await readJson(pkgPath)) as { version: string };
25
- return `^${pkg.version}`;
26
- }
27
-
28
13
  export async function createWorkspaceCommand(opts: CreateWorkspaceOptions): Promise<void> {
29
14
  if (opts.json) {
30
15
  setOutputMode('json');
@@ -34,7 +19,6 @@ export async function createWorkspaceCommand(opts: CreateWorkspaceOptions): Prom
34
19
  const templatesDir = getTemplatesDir();
35
20
  const wsTemplatesDir = path.join(templatesDir, 'workspace');
36
21
 
37
- // Refuse non-empty target.
38
22
  if (await pathExists(home)) {
39
23
  const entries = await readdir(home);
40
24
  if (entries.length > 0) {
@@ -44,24 +28,13 @@ export async function createWorkspaceCommand(opts: CreateWorkspaceOptions): Prom
44
28
  await ensureDir(home);
45
29
  }
46
30
 
47
- const [cliVersion, pkgRaw] = await Promise.all([
48
- readCurrentCliVersion(),
49
- readFile(path.join(wsTemplatesDir, 'package.json.tmpl'), 'utf8'),
50
- ]);
31
+ const pkgRaw = await readFile(path.join(wsTemplatesDir, 'package.json.tmpl'), 'utf8');
51
32
 
52
- // Sources have plain names (gitignore, yarnrc.yml, nvmrc, npmrc) so they
53
- // aren't interpreted as the active config files of *this* package; we
54
- // rename to dotfiles at the destination.
55
- //
56
- // Yarn berry ships inline at .yarn/releases/. The workspace's .yarnrc.yml
57
- // sets `yarnPath` to point here, so any `yarn` invocation (system classic
58
- // or otherwise) reads the rc, delegates to this binary, and runs as 4.5.1
59
- // — no corepack, no global pin needed. Mirrors how bigbrain-z2h ships its
60
- // own yarn release.
61
33
  await Promise.all([
62
- writeFile(path.join(home, 'package.json'), renderTemplate(pkgRaw, { __Z2H_CLI_VERSION__: cliVersion })),
34
+ writeFile(path.join(home, 'package.json'), pkgRaw),
63
35
  copy(path.join(wsTemplatesDir, 'gitignore'), path.join(home, '.gitignore')),
64
36
  copy(path.join(wsTemplatesDir, 'yarnrc.yml'), path.join(home, '.yarnrc.yml')),
37
+ copy(path.join(wsTemplatesDir, 'nx.json'), path.join(home, 'nx.json')),
65
38
  copy(path.join(wsTemplatesDir, 'yarnlock'), path.join(home, 'yarn.lock')),
66
39
  copy(path.join(wsTemplatesDir, 'nvmrc'), path.join(home, '.nvmrc')),
67
40
  copy(path.join(wsTemplatesDir, 'npmrc'), path.join(home, '.npmrc')),
@@ -81,6 +54,5 @@ export async function createWorkspaceCommand(opts: CreateWorkspaceOptions): Prom
81
54
  output(`Your workspace is ready! 🚀 Apps will live at ${home}`, {
82
55
  ok: true,
83
56
  workspace: home,
84
- z2hCliVersion: cliVersion,
85
57
  });
86
58
  }
@@ -0,0 +1,15 @@
1
+ import { setOutputMode } from '../util/logger';
2
+ import { autoMigrate } from '../migrations/auto-migrate';
3
+
4
+ export interface MigrateOptions {
5
+ dryRun?: boolean;
6
+ json?: boolean;
7
+ }
8
+
9
+ export async function migrateCommand(cliVersion: string, opts: MigrateOptions): Promise<void> {
10
+ if (opts.json) {
11
+ setOutputMode('json');
12
+ }
13
+
14
+ await autoMigrate(cliVersion, opts.dryRun);
15
+ }
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ import { deployCommand } from './commands/deploy';
7
7
  import { createWorkspaceCommand } from './commands/create-workspace';
8
8
  import { createCommand } from './commands/create';
9
9
  import { generateZ2HTokenCommand } from './commands/generate-z2h-token';
10
+ import { migrateCommand } from './commands/migrate';
10
11
  import { errorLog, fail } from './util/logger';
11
12
 
12
13
  const { version } = createRequire(import.meta.url)('../package.json') as { version: string };
@@ -125,6 +126,19 @@ async function main(): Promise<void> {
125
126
  }
126
127
  });
127
128
 
129
+ program
130
+ .command('migrate')
131
+ .description('Run pending workspace migrations')
132
+ .option('--dry-run', 'show pending migrations without applying any changes')
133
+ .option('--json', 'emit structured JSON on stdout')
134
+ .action(async (opts: { dryRun?: boolean; json?: boolean }) => {
135
+ try {
136
+ await migrateCommand(version, opts);
137
+ } catch (err) {
138
+ fail(`migrate failed: ${(err as Error).message}`, { command: 'migrate' }, 1);
139
+ }
140
+ });
141
+
128
142
  await program.parseAsync(process.argv);
129
143
  }
130
144
 
@@ -0,0 +1,15 @@
1
+ import type { Tree } from '@nx/devkit';
2
+
3
+ export default function removePerConsumerYarnrc(tree: Tree): void {
4
+ for (const entry of tree.children('.')) {
5
+ if (entry.startsWith('.')) {
6
+ continue;
7
+ }
8
+ const pkgPath = `${entry}/package.json`;
9
+ const yarnrcPath = `${entry}/.yarnrc.yml`;
10
+ if (!tree.exists(pkgPath) || !tree.exists(yarnrcPath)) {
11
+ continue;
12
+ }
13
+ tree.delete(yarnrcPath);
14
+ }
15
+ }
@@ -0,0 +1,47 @@
1
+ import type { Tree } from '@nx/devkit';
2
+
3
+ export default function removeZ2hCliFromWorkspaceDeps(tree: Tree): void {
4
+ // Clean workspace root package.json
5
+ if (tree.exists('package.json')) {
6
+ const pkg = JSON.parse(tree.read('package.json', 'utf-8')!);
7
+ let changed = false;
8
+
9
+ if (pkg.devDependencies?.['@mondaydotcomorg/z2h-cli']) {
10
+ delete pkg.devDependencies['@mondaydotcomorg/z2h-cli'];
11
+ changed = true;
12
+ }
13
+
14
+ if (pkg.resolutions?.['@mondaydotcomorg/z2h-cli']) {
15
+ delete pkg.resolutions['@mondaydotcomorg/z2h-cli'];
16
+ if (Object.keys(pkg.resolutions).length === 0) {
17
+ delete pkg.resolutions;
18
+ }
19
+ changed = true;
20
+ }
21
+
22
+ if (changed) {
23
+ tree.write('package.json', JSON.stringify(pkg, null, 2) + '\n');
24
+ }
25
+ }
26
+
27
+ // Clean each consumer app's package.json (remove devDep, keep scripts)
28
+ for (const entry of tree.children('.')) {
29
+ if (entry.startsWith('.')) {
30
+ continue;
31
+ }
32
+ const pkgPath = `${entry}/package.json`;
33
+ if (!tree.exists(pkgPath)) {
34
+ continue;
35
+ }
36
+
37
+ const pkg = JSON.parse(tree.read(pkgPath, 'utf-8')!);
38
+
39
+ if (pkg.devDependencies?.['@mondaydotcomorg/z2h-cli']) {
40
+ delete pkg.devDependencies['@mondaydotcomorg/z2h-cli'];
41
+ if (Object.keys(pkg.devDependencies).length === 0) {
42
+ delete pkg.devDependencies;
43
+ }
44
+ tree.write(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,214 @@
1
+ import path from 'node:path';
2
+ import { createRequire } from 'node:module';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { pathExists, readJson, writeJson, writeFile } from 'fs-extra';
5
+ import { execa } from 'execa';
6
+ import semver from 'semver';
7
+ import { info, warn } from '../util/logger';
8
+ import { getWorkspaceHome, isZ2hWorkspace } from '../util/workspace';
9
+
10
+ const STATE_FILE = '.z2h-migrations-state.json';
11
+ const NX_JSON_CONTENT = '{\n "extends": "@mondaydotcomorg/trident-monorepo/presets/nx.json"\n}\n';
12
+
13
+ interface MigrationState {
14
+ lastMigratedVersion: string;
15
+ }
16
+
17
+ interface MigrationEntry {
18
+ version: string;
19
+ description: string;
20
+ migrate: string;
21
+ }
22
+
23
+ interface MigrationsJson {
24
+ migrations: Record<string, MigrationEntry>;
25
+ }
26
+
27
+ // ─── Helpers ───
28
+
29
+ async function readState(workspaceHome: string): Promise<MigrationState | null> {
30
+ const statePath = path.join(workspaceHome, STATE_FILE);
31
+ if (!(await pathExists(statePath))) {
32
+ return null;
33
+ }
34
+ try {
35
+ return (await readJson(statePath)) as MigrationState;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ async function writeState(workspaceHome: string, state: MigrationState): Promise<void> {
42
+ const statePath = path.join(workspaceHome, STATE_FILE);
43
+ await writeJson(statePath, state, { spaces: 2 });
44
+ }
45
+
46
+ async function yarnInstall(workspaceHome: string, context: string): Promise<boolean> {
47
+ info(`${context} — running yarn install...`);
48
+ try {
49
+ await execa('yarn', ['install'], { cwd: workspaceHome, stdio: 'pipe' });
50
+ return true;
51
+ } catch (err) {
52
+ const stderr = (err as { stderr?: string }).stderr || (err as Error).message;
53
+ warn(`yarn install failed: ${stderr}`);
54
+ return false;
55
+ }
56
+ }
57
+
58
+ async function getLatestTrientMonorepoVersion(): Promise<string> {
59
+ try {
60
+ const { stdout } = await execa('npm', ['view', '@mondaydotcomorg/trident-monorepo', 'version']);
61
+ return `^${stdout.trim()}`;
62
+ } catch {
63
+ return '*';
64
+ }
65
+ }
66
+
67
+ // ─── Workspace bootstrap ───
68
+
69
+ function isNxAvailable(workspaceHome: string): boolean {
70
+ try {
71
+ const wsRequire = createRequire(path.join(workspaceHome, 'package.json'));
72
+ wsRequire.resolve('nx/src/generators/tree');
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
79
+ async function ensureWorkspaceNx(workspaceHome: string): Promise<boolean> {
80
+ const nxJsonPath = path.join(workspaceHome, 'nx.json');
81
+ const pkgPath = path.join(workspaceHome, 'package.json');
82
+
83
+ if (!(await pathExists(nxJsonPath))) {
84
+ await writeFile(nxJsonPath, NX_JSON_CONTENT);
85
+ info('added nx.json to workspace');
86
+ }
87
+
88
+ const pkg = (await readJson(pkgPath)) as {
89
+ devDependencies?: Record<string, string>;
90
+ scripts?: Record<string, string>;
91
+ };
92
+
93
+ if (!pkg.devDependencies?.['@mondaydotcomorg/trident-monorepo']) {
94
+ pkg.devDependencies = pkg.devDependencies ?? {};
95
+ pkg.devDependencies['@mondaydotcomorg/trident-monorepo'] = await getLatestTrientMonorepoVersion();
96
+ }
97
+
98
+ if (!pkg.scripts?.nx) {
99
+ pkg.scripts = pkg.scripts ?? {};
100
+ pkg.scripts.nx = 'nx';
101
+ }
102
+
103
+ await writeJson(pkgPath, pkg, { spaces: 2 });
104
+
105
+ // Only install if NX isn't already importable from the workspace
106
+ if (!isNxAvailable(workspaceHome)) {
107
+ const ok = await yarnInstall(workspaceHome, 'NX not found, installing');
108
+ if (!ok) {
109
+ warn('workspace migrations require NX. Fix the install error and run "z2h-cli migrate" to retry.');
110
+ return false;
111
+ }
112
+ }
113
+
114
+ return true;
115
+ }
116
+
117
+ // ─── Main ───
118
+
119
+ export async function autoMigrate(cliVersion: string, dryRun = false): Promise<void> {
120
+ const workspaceHome = getWorkspaceHome();
121
+ if (!(await isZ2hWorkspace(workspaceHome))) {
122
+ warn('no Z2H workspace found');
123
+ return;
124
+ }
125
+
126
+ const state = await readState(workspaceHome);
127
+ const lastVersion = state?.lastMigratedVersion ?? '0.0.0';
128
+
129
+ if (!semver.gt(cliVersion, lastVersion)) {
130
+ info('workspace is up to date — no migrations pending');
131
+ return;
132
+ }
133
+
134
+ info(`migrating workspace from v${lastVersion} → v${cliVersion}...`);
135
+
136
+ // migrations.json is at the package root — two dirs up from dist/migrations/
137
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
138
+ const migrationsJsonPath = path.resolve(thisDir, '..', '..', 'migrations.json');
139
+
140
+ if (!(await pathExists(migrationsJsonPath))) {
141
+ warn('migrations.json not found — skipping migrations');
142
+ return;
143
+ }
144
+
145
+ const migrationsJson = (await readJson(migrationsJsonPath)) as MigrationsJson;
146
+
147
+ // Filter to pending migrations (version > lastVersion AND <= cliVersion)
148
+ const pending = Object.entries(migrationsJson.migrations)
149
+ .filter(([, entry]) => semver.gt(entry.version, lastVersion) && semver.lte(entry.version, cliVersion))
150
+ .sort(([, a], [, b]) => semver.compare(a.version, b.version));
151
+
152
+ if (dryRun) {
153
+ info(`${pending.length} pending migration(s) (dry-run — no changes will be applied):`);
154
+ for (const [, entry] of pending) {
155
+ info(` - ${entry.description} (v${entry.version})`);
156
+ }
157
+ return;
158
+ }
159
+
160
+ if (pending.length === 0) {
161
+ await writeState(workspaceHome, { lastMigratedVersion: cliVersion });
162
+ return;
163
+ }
164
+
165
+ const nxReady = await ensureWorkspaceNx(workspaceHome);
166
+ if (!nxReady) {
167
+ return;
168
+ }
169
+
170
+ info(`applying ${pending.length} workspace migration(s)...`);
171
+
172
+ // Resolve NX from the workspace's node_modules (not from CLI's own deps)
173
+ const wsRequire = createRequire(path.join(workspaceHome, 'package.json'));
174
+ const { FsTree, flushChanges } = wsRequire('nx/src/generators/tree') as {
175
+ FsTree: new (root: string, isVerbose: boolean) => import('@nx/devkit').Tree & {
176
+ listChanges(): { type: string; path: string }[];
177
+ };
178
+ flushChanges: (root: string, changes: { type: string; path: string; content?: Buffer | null }[]) => void;
179
+ };
180
+
181
+ let needsInstall = false;
182
+
183
+ for (const [name, entry] of pending) {
184
+ const tree = new FsTree(workspaceHome, false);
185
+ try {
186
+ const factoryPath = path.resolve(path.dirname(migrationsJsonPath), entry.migrate);
187
+ const mod = require(factoryPath);
188
+ const fn = mod.default || mod;
189
+ fn(tree);
190
+
191
+ const changes = tree.listChanges();
192
+ if (changes.length > 0) {
193
+ if (changes.some(c => c.path.endsWith('package.json'))) {
194
+ needsInstall = true;
195
+ }
196
+ flushChanges(workspaceHome, changes);
197
+ info(` ✓ ${entry.description}`);
198
+ } else {
199
+ info(` ○ ${entry.description} (no changes needed)`);
200
+ }
201
+ } catch (err) {
202
+ warn(` ✗ migration "${name}" failed: ${(err as Error).message}`);
203
+ warn(' skipping remaining migrations. Run "z2h-cli migrate" to retry.');
204
+ return;
205
+ }
206
+ }
207
+
208
+ if (needsInstall) {
209
+ await yarnInstall(workspaceHome, 'package.json changed');
210
+ }
211
+
212
+ await writeState(workspaceHome, { lastMigratedVersion: cliVersion });
213
+ info('workspace migrations complete.');
214
+ }
@@ -27,7 +27,7 @@ The orchestrator routes to `z2h:feature-development`, which handles the typechec
27
27
 
28
28
  ## 🌟 Data-Fetching Hierarchy
29
29
 
30
- When a request needs data, follow `z2h:coding-standards-mf` §0: write a `runQuery` helper for Snowflake data (TS module using `getBigBrainAPI()` for React apps; vanilla `<script>` using `window.location.origin` for html-embed apps), write a client-side fetch for other browser-accessible sources, or tell the user to reach out at `#zero-to-hero` if the data needs an API key or server-side access. Backend changes to `bigbrain-zth` are not in scope.
30
+ When a request needs data, follow `z2h:coding-standards-mf` §0: write a `runQuery` helper for Snowflake data (TS module using `getBigBrainAPI()` for React apps; vanilla `<script>` using `window.location.origin` for html-embed apps), write a client-side fetch for other browser-accessible sources, or tell the user to reach out at `#ask-zero-to-hero` if the data needs an API key or server-side access. Backend changes to `bigbrain-zth` are not in scope.
31
31
 
32
32
  ---
33
33