@mondaydotcomorg/z2h-cli 0.15.0 → 0.15.2-feat-eitantsh-z2h-admin-panel-package-388aad6

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 (115) hide show
  1. package/dist/commands/build.d.ts +0 -1
  2. package/dist/commands/build.d.ts.map +1 -1
  3. package/dist/commands/build.js +0 -3
  4. package/dist/commands/clean.d.ts +1 -4
  5. package/dist/commands/clean.d.ts.map +1 -1
  6. package/dist/commands/clean.js +1 -4
  7. package/dist/commands/create-workspace.d.ts +1 -4
  8. package/dist/commands/create-workspace.d.ts.map +1 -1
  9. package/dist/commands/create-workspace.js +1 -4
  10. package/dist/commands/create.d.ts +0 -1
  11. package/dist/commands/create.d.ts.map +1 -1
  12. package/dist/commands/create.js +0 -3
  13. package/dist/commands/delete.d.ts +0 -1
  14. package/dist/commands/delete.d.ts.map +1 -1
  15. package/dist/commands/delete.js +1 -4
  16. package/dist/commands/deploy.d.ts +0 -1
  17. package/dist/commands/deploy.d.ts.map +1 -1
  18. package/dist/commands/deploy.js +4 -7
  19. package/dist/commands/dev.d.ts +0 -1
  20. package/dist/commands/dev.d.ts.map +1 -1
  21. package/dist/commands/dev.js +0 -3
  22. package/dist/commands/doctor.d.ts +1 -4
  23. package/dist/commands/doctor.d.ts.map +1 -1
  24. package/dist/commands/doctor.js +2 -5
  25. package/dist/commands/edit.d.ts +1 -4
  26. package/dist/commands/edit.d.ts.map +1 -1
  27. package/dist/commands/edit.js +1 -4
  28. package/dist/commands/generate-z2h-token.d.ts +0 -1
  29. package/dist/commands/generate-z2h-token.d.ts.map +1 -1
  30. package/dist/commands/generate-z2h-token.js +0 -3
  31. package/dist/commands/grant.d.ts +1 -5
  32. package/dist/commands/grant.d.ts.map +1 -1
  33. package/dist/commands/grant.js +1 -10
  34. package/dist/commands/migrate.d.ts +0 -1
  35. package/dist/commands/migrate.d.ts.map +1 -1
  36. package/dist/commands/migrate.js +106 -4
  37. package/dist/commands/tag.d.ts +0 -1
  38. package/dist/commands/tag.d.ts.map +1 -1
  39. package/dist/commands/tag.js +1 -4
  40. package/dist/esm/commands/build.d.ts +0 -1
  41. package/dist/esm/commands/build.d.ts.map +1 -1
  42. package/dist/esm/commands/build.mjs +1 -4
  43. package/dist/esm/commands/clean.d.ts +1 -4
  44. package/dist/esm/commands/clean.d.ts.map +1 -1
  45. package/dist/esm/commands/clean.mjs +2 -5
  46. package/dist/esm/commands/create-workspace.d.ts +1 -4
  47. package/dist/esm/commands/create-workspace.d.ts.map +1 -1
  48. package/dist/esm/commands/create-workspace.mjs +2 -5
  49. package/dist/esm/commands/create.d.ts +0 -1
  50. package/dist/esm/commands/create.d.ts.map +1 -1
  51. package/dist/esm/commands/create.mjs +1 -4
  52. package/dist/esm/commands/delete.d.ts +0 -1
  53. package/dist/esm/commands/delete.d.ts.map +1 -1
  54. package/dist/esm/commands/delete.mjs +2 -5
  55. package/dist/esm/commands/deploy.d.ts +0 -1
  56. package/dist/esm/commands/deploy.d.ts.map +1 -1
  57. package/dist/esm/commands/deploy.mjs +5 -8
  58. package/dist/esm/commands/dev.d.ts +0 -1
  59. package/dist/esm/commands/dev.d.ts.map +1 -1
  60. package/dist/esm/commands/dev.mjs +1 -4
  61. package/dist/esm/commands/doctor.d.ts +1 -4
  62. package/dist/esm/commands/doctor.d.ts.map +1 -1
  63. package/dist/esm/commands/doctor.mjs +3 -6
  64. package/dist/esm/commands/edit.d.ts +1 -4
  65. package/dist/esm/commands/edit.d.ts.map +1 -1
  66. package/dist/esm/commands/edit.mjs +2 -5
  67. package/dist/esm/commands/generate-z2h-token.d.ts +0 -1
  68. package/dist/esm/commands/generate-z2h-token.d.ts.map +1 -1
  69. package/dist/esm/commands/generate-z2h-token.mjs +1 -4
  70. package/dist/esm/commands/grant.d.ts +1 -5
  71. package/dist/esm/commands/grant.d.ts.map +1 -1
  72. package/dist/esm/commands/grant.mjs +2 -11
  73. package/dist/esm/commands/migrate.d.ts +0 -1
  74. package/dist/esm/commands/migrate.d.ts.map +1 -1
  75. package/dist/esm/commands/migrate.mjs +101 -5
  76. package/dist/esm/commands/tag.d.ts +0 -1
  77. package/dist/esm/commands/tag.d.ts.map +1 -1
  78. package/dist/esm/commands/tag.mjs +2 -5
  79. package/dist/esm/index.mjs +20 -29
  80. package/dist/esm/util/tracker.d.ts.map +1 -1
  81. package/dist/esm/util/tracker.mjs +4 -1
  82. package/dist/esm/util/workspace.d.ts +3 -0
  83. package/dist/esm/util/workspace.d.ts.map +1 -1
  84. package/dist/esm/util/workspace.mjs +64 -2
  85. package/dist/index.js +19 -28
  86. package/dist/util/tracker.d.ts.map +1 -1
  87. package/dist/util/tracker.js +4 -1
  88. package/dist/util/workspace.d.ts +3 -0
  89. package/dist/util/workspace.d.ts.map +1 -1
  90. package/dist/util/workspace.js +65 -0
  91. package/package.json +1 -1
  92. package/src/commands/__tests__/doctor.test.ts +12 -5
  93. package/src/commands/build.ts +1 -6
  94. package/src/commands/clean.ts +2 -9
  95. package/src/commands/create-workspace.ts +2 -10
  96. package/src/commands/create.ts +1 -6
  97. package/src/commands/delete.ts +2 -7
  98. package/src/commands/deploy.ts +5 -10
  99. package/src/commands/dev.ts +1 -6
  100. package/src/commands/doctor.ts +3 -11
  101. package/src/commands/edit.ts +2 -10
  102. package/src/commands/generate-z2h-token.ts +1 -6
  103. package/src/commands/grant.ts +2 -16
  104. package/src/commands/migrate.ts +139 -6
  105. package/src/commands/tag.ts +2 -7
  106. package/src/index.ts +29 -41
  107. package/src/util/tracker.ts +6 -1
  108. package/src/util/workspace.ts +75 -1
  109. package/dist/esm/migrations/auto-migrate.d.ts +0 -2
  110. package/dist/esm/migrations/auto-migrate.d.ts.map +0 -1
  111. package/dist/esm/migrations/auto-migrate.mjs +0 -169
  112. package/dist/migrations/auto-migrate.d.ts +0 -2
  113. package/dist/migrations/auto-migrate.d.ts.map +0 -1
  114. package/dist/migrations/auto-migrate.js +0 -177
  115. package/src/migrations/auto-migrate.ts +0 -217
@@ -1,21 +1,13 @@
1
1
  import path from 'node:path';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { ensureDir, writeFile, pathExists, readdir, copy } from 'fs-extra';
4
- import { info, output, setOutputMode } from '../util/logger';
4
+ import { info, output } from '../util/logger';
5
5
  import { getWorkspaceHome } from '../util/workspace';
6
6
  import { getTemplatesDir } from '../util/templates';
7
7
  import { runYarnInstall } from '../util/npm/install';
8
8
  import { workspaceNpmrcContents, workspaceYarnrcContents } from '../util/npm/broker-config';
9
9
 
10
- export interface CreateWorkspaceOptions {
11
- json?: boolean;
12
- }
13
-
14
- export async function createWorkspaceCommand(opts: CreateWorkspaceOptions = {}): Promise<void> {
15
- if (opts.json) {
16
- setOutputMode('json');
17
- }
18
-
10
+ export async function createWorkspaceCommand(): Promise<void> {
19
11
  const home = getWorkspaceHome();
20
12
  const templatesDir = getTemplatesDir();
21
13
  const wsTemplatesDir = path.join(templatesDir, 'workspace');
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  import { ensureDir, pathExists, readdir, writeFile, copy } from 'fs-extra';
4
- import { info, warn, output, setOutputMode } from '../util/logger';
4
+ import { info, warn, output } from '../util/logger';
5
5
  import { assertWorkspace } from '../util/workspace';
6
6
  import { getTemplatesDir, renderTemplate, APP_TEMPLATES, type AppTemplate } from '../util/templates';
7
7
  import { APP_NAME_PATTERN } from '../constants';
@@ -19,7 +19,6 @@ export interface CreateOptions {
19
19
  htmlFile?: string;
20
20
  remote?: RemoteType;
21
21
  repoUrl?: string;
22
- json?: boolean;
23
22
  }
24
23
 
25
24
  // Names reserved by the host MF (mf-bigbrain-zth) for platform pages under
@@ -50,10 +49,6 @@ async function isDirEmpty(dir: string): Promise<boolean> {
50
49
  }
51
50
 
52
51
  export async function createCommand(appName: string, opts: CreateOptions = {}): Promise<void> {
53
- if (opts.json) {
54
- setOutputMode('json');
55
- }
56
-
57
52
  // Validate workspace.
58
53
  const workspace = await assertWorkspace();
59
54
 
@@ -1,12 +1,11 @@
1
1
  import { createInterface } from 'node:readline';
2
- import { output, info, setOutputMode } from '../util/logger';
2
+ import { output, info, getOutputMode } from '../util/logger';
3
3
  import { getCwdAppName, cleanAppName, mfAppName } from '../util/app-name';
4
4
  import { brokerBaseUrl, routingHeaders } from '../constants';
5
5
  import { readZthAuth } from '../util/auth/auth-store';
6
6
 
7
7
  export interface DeleteOptions {
8
8
  yes?: boolean;
9
- json?: boolean;
10
9
  }
11
10
 
12
11
  // Type-the-app-name confirmation. The caller must retype the de-scoped app
@@ -26,10 +25,6 @@ async function confirmByName(appSlug: string): Promise<boolean> {
26
25
 
27
26
  // Should be run from the consumer app directory — uses process.cwd() to locate it.
28
27
  export async function deleteCommand(opts: DeleteOptions = {}): Promise<void> {
29
- if (opts.json) {
30
- setOutputMode('json');
31
- }
32
-
33
28
  const cwdName = getCwdAppName();
34
29
  if (!cwdName) {
35
30
  throw new Error('Run this from inside a consumer app directory (the one with your package.json).');
@@ -39,7 +34,7 @@ export async function deleteCommand(opts: DeleteOptions = {}): Promise<void> {
39
34
 
40
35
  // Confirmation. --json is always non-interactive, so it requires --yes.
41
36
  if (!opts.yes) {
42
- if (opts.json || !process.stdin.isTTY) {
37
+ if (getOutputMode() === 'json' || !process.stdin.isTTY) {
43
38
  throw new Error('Refusing to delete without confirmation — pass --yes to delete non-interactively.');
44
39
  }
45
40
  if (!(await confirmByName(appSlug))) {
@@ -2,7 +2,7 @@ import path from 'node:path';
2
2
  import os from 'node:os';
3
3
  import { createInterface } from 'node:readline';
4
4
  import { pathExists, readJson } from 'fs-extra';
5
- import { info, warn, output, setOutputMode, getOutputMode } from '../util/logger';
5
+ import { info, warn, output, getOutputMode } from '../util/logger';
6
6
  import { resolveConsumerPaths } from '../shadow/paths';
7
7
  import { fetchAppTags, fetchTagCatalog, setAppTags } from '../util/tags';
8
8
  import { type AssetManifestFile, type ManifestEntry } from '../util/manifest';
@@ -21,17 +21,12 @@ import { buildCommand } from './build';
21
21
 
22
22
  export interface DeployOptions {
23
23
  dryRun?: boolean;
24
- json?: boolean;
25
24
  remote?: RemoteType;
26
25
  repoUrl?: string;
27
26
  }
28
27
 
29
28
  // Should be run from the consumer app directory — uses process.cwd() to locate it.
30
29
  export async function deployCommand(opts: DeployOptions = {}): Promise<void> {
31
- if (opts.json) {
32
- setOutputMode('json');
33
- }
34
-
35
30
  const paths = resolveConsumerPaths(process.cwd());
36
31
  const consumerPkg = (await readJson(paths.consumerPkg)) as { name?: string };
37
32
  if (!consumerPkg.name) {
@@ -90,7 +85,7 @@ export async function deployCommand(opts: DeployOptions = {}): Promise<void> {
90
85
  const url = `${BUCKET_READ_DNS_URL}/${keyPrefix}`;
91
86
 
92
87
  info('building');
93
- await buildCommand({ json: opts.json, publicUrl: url });
88
+ await buildCommand({ publicUrl: url });
94
89
 
95
90
  if (!(await pathExists(paths.shadowBuild))) {
96
91
  throw new Error(`No build output at ${paths.shadowBuild} after build step.`);
@@ -149,7 +144,7 @@ export async function deployCommand(opts: DeployOptions = {}): Promise<void> {
149
144
  // The two are separate endpoints, so the CLI picks based on the version it
150
145
  // was handed.
151
146
  if (nextVersion === 1) {
152
- const visibility = await resolveFirstDeployVisibility(appName, opts.json);
147
+ const visibility = await resolveFirstDeployVisibility(appName);
153
148
  info(`registering app "${appName}" → v${nextVersion} (${visibility})`);
154
149
  await registerApp(appName, token, nextVersion, entry, visibility, gitRemote);
155
150
  } else {
@@ -262,8 +257,8 @@ async function updateApp(
262
257
  // First deploy only: decide whether the app is born public or private. The
263
258
  // choice is passed to the register endpoint (default private). Non-interactive
264
259
  // runs (--json or no TTY) always default to private.
265
- async function resolveFirstDeployVisibility(appName: string, json?: boolean): Promise<'public' | 'private'> {
266
- if (json || !process.stdout.isTTY) {
260
+ async function resolveFirstDeployVisibility(appName: string): Promise<'public' | 'private'> {
261
+ if (getOutputMode() === 'json' || !process.stdout.isTTY) {
267
262
  return 'private';
268
263
  }
269
264
 
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import { readJson, writeFile } from 'fs-extra';
4
4
  import { watch } from 'chokidar';
5
5
  import { execa } from 'execa';
6
- import { info, warn, errorLog, output, setOutputMode } from '../util/logger';
6
+ import { info, warn, errorLog, output } from '../util/logger';
7
7
  import { prepareShadow } from '../shadow/prepare';
8
8
  import { ensureConsumerPkg } from '../shadow/consumer-pkg';
9
9
  import { syncShadow } from '../shadow/sync';
@@ -11,7 +11,6 @@ import { openInBrowser } from '../util/open-browser';
11
11
  import type { ConsumerPkg } from '../types';
12
12
 
13
13
  export interface DevOptions {
14
- json?: boolean;
15
14
  open?: boolean;
16
15
  }
17
16
 
@@ -19,10 +18,6 @@ const TRIDENT_READY_PATTERN = /Server started at/i;
19
18
 
20
19
  // Should be run from the consumer app directory — uses process.cwd() to locate it.
21
20
  export async function devCommand(opts: DevOptions = {}): Promise<void> {
22
- if (opts.json) {
23
- setOutputMode('json');
24
- }
25
-
26
21
  const ctx = await prepareShadow();
27
22
  const { port } = ctx;
28
23
  const url = `https://bigbrain.me/bigbrain-vibe/${ctx.appName}?zth_local=${port}`;
@@ -6,7 +6,7 @@ import { execa } from 'execa';
6
6
  import semver from 'semver';
7
7
  import { pathExists, readJson } from 'fs-extra';
8
8
  import pc from 'picocolors';
9
- import { setOutputMode, output } from '../util/logger';
9
+ import { output, getOutputMode } from '../util/logger';
10
10
  import { isZ2hWorkspace, getWorkspaceHome } from '../util/workspace';
11
11
  import { Z2H_AUTH_FILE, tryReadAuthTokenSync } from '../util/auth/auth-store';
12
12
  import { brokerNpmRegistryUrl, routingHeaders } from '../constants';
@@ -26,10 +26,6 @@ const SSL_CERT_PATH = path.join(
26
26
  'certificate.crt'
27
27
  );
28
28
 
29
- export interface DoctorOptions {
30
- json?: boolean;
31
- }
32
-
33
29
  type Status = 'ok' | 'warn' | 'fail';
34
30
 
35
31
  interface CheckResult {
@@ -317,11 +313,7 @@ function renderCheck(c: CheckResult): void {
317
313
 
318
314
  // ── command ───────────────────────────────────────────────────────────────────
319
315
 
320
- export async function doctorCommand(opts: DoctorOptions = {}): Promise<void> {
321
- if (opts.json) {
322
- setOutputMode('json');
323
- }
324
-
316
+ export async function doctorCommand(): Promise<void> {
325
317
  let pluginsJson: InstalledPluginsJson | null = null;
326
318
  try {
327
319
  if (await pathExists(CLAUDE_PLUGINS_JSON)) {
@@ -350,7 +342,7 @@ export async function doctorCommand(opts: DoctorOptions = {}): Promise<void> {
350
342
  const allChecks = sections.flatMap(s => s.checks);
351
343
  const issues = allChecks.filter(c => c.status !== 'ok');
352
344
 
353
- if (opts.json) {
345
+ if (getOutputMode() === 'json') {
354
346
  output('', {
355
347
  ok: issues.filter(c => c.status === 'fail').length === 0,
356
348
  checks: allChecks,
@@ -1,6 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { pathExists } from 'fs-extra';
3
- import { info, warn, output, setOutputMode } from '../util/logger';
3
+ import { info, warn, output } from '../util/logger';
4
4
  import { assertWorkspace } from '../util/workspace';
5
5
  import { mfAppName } from '../util/app-name';
6
6
  import { fetchApp } from '../util/broker/app';
@@ -9,15 +9,7 @@ import { cloneRepo, pullAndResolve } from '../util/git/repo';
9
9
  import { runYarnInstall } from '../util/npm/install';
10
10
  import { prepareShadow } from '../shadow/prepare';
11
11
 
12
- export interface EditOptions {
13
- json?: boolean;
14
- }
15
-
16
- export async function editCommand(appName: string, opts: EditOptions = {}): Promise<void> {
17
- if (opts.json) {
18
- setOutputMode('json');
19
- }
20
-
12
+ export async function editCommand(appName: string): Promise<void> {
21
13
  const workspace = await assertWorkspace();
22
14
  const mfName = mfAppName(appName);
23
15
 
@@ -1,6 +1,6 @@
1
1
  import readline from 'node:readline/promises';
2
2
  import { stdin, stdout } from 'node:process';
3
- import { info, warn, output, setOutputMode } from '../util/logger';
3
+ import { info, warn, output } from '../util/logger';
4
4
  import { writeZthAuth, validateJwtPattern } from '../util/auth/write-auth';
5
5
  import { startLoopbackServer } from '../util/auth/loopback-server';
6
6
  import { openInBrowser } from '../util/open-browser';
@@ -18,7 +18,6 @@ export interface GenerateZ2HTokenOptions {
18
18
  noBrowser?: boolean;
19
19
  /** Override the page URL — useful when pointing at staging. */
20
20
  url?: string;
21
- json?: boolean;
22
21
  }
23
22
 
24
23
  async function promptForToken(): Promise<string> {
@@ -76,10 +75,6 @@ async function runLoopbackFlow(pageUrl: string): Promise<string | null> {
76
75
  }
77
76
 
78
77
  export async function generateZ2HTokenCommand(opts: GenerateZ2HTokenOptions = {}): Promise<void> {
79
- if (opts.json) {
80
- setOutputMode('json');
81
- }
82
-
83
78
  const pageUrl = opts.url ?? process.env.Z2H_TOKEN_PAGE_URL ?? DEFAULT_TOKEN_PAGE_URL;
84
79
 
85
80
  // --no-browser bypasses both auto-open and the loopback round-trip — the
@@ -1,4 +1,4 @@
1
- import { output, setOutputMode } from '../util/logger';
1
+ import { output } from '../util/logger';
2
2
  import { getCwdAppName, mfAppName } from '../util/app-name';
3
3
  import { brokerBaseUrl, routingHeaders } from '../constants';
4
4
  import { readZthAuth } from '../util/auth/auth-store';
@@ -6,7 +6,6 @@ import { readZthAuth } from '../util/auth/auth-store';
6
6
  export interface GrantOptions {
7
7
  public?: boolean;
8
8
  level?: 'owner' | 'editor' | 'viewer';
9
- json?: boolean;
10
9
  }
11
10
 
12
11
  async function postGrant(
@@ -26,9 +25,6 @@ async function postGrant(
26
25
  }
27
26
 
28
27
  export async function grantCommand(email: string | undefined, opts: GrantOptions): Promise<void> {
29
- if (opts.json) {
30
- setOutputMode('json');
31
- }
32
28
  if (!opts.public && !email) {
33
29
  throw new Error('Specify what to grant — e.g. --public or provide an email');
34
30
  }
@@ -59,9 +55,6 @@ export async function grantCommand(email: string | undefined, opts: GrantOptions
59
55
  }
60
56
 
61
57
  export async function revokeCommand(email: string | undefined, opts: GrantOptions): Promise<void> {
62
- if (opts.json) {
63
- setOutputMode('json');
64
- }
65
58
  if (!opts.public && !email) {
66
59
  throw new Error('Specify what to revoke — e.g. --public or provide an email');
67
60
  }
@@ -86,14 +79,7 @@ export async function revokeCommand(email: string | undefined, opts: GrantOption
86
79
  output(msg, { ok: true, appName, revoked });
87
80
  }
88
81
 
89
- export interface TransferOwnerOptions {
90
- json?: boolean;
91
- }
92
-
93
- export async function transferOwnerCommand(email: string | undefined, opts: TransferOwnerOptions): Promise<void> {
94
- if (opts.json) {
95
- setOutputMode('json');
96
- }
82
+ export async function transferOwnerCommand(email: string | undefined): Promise<void> {
97
83
  if (!email) {
98
84
  throw new Error('Specify the new owner — e.g. z2h-cli transfer-owner someone@monday.com');
99
85
  }
@@ -1,15 +1,148 @@
1
- import { setOutputMode } from '../util/logger';
2
- import { autoMigrate } from '../migrations/auto-migrate';
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { pathExists, readJson, writeJson } from 'fs-extra';
5
+ import semver from 'semver';
6
+ import { errorLog, info, warn } from '../util/logger';
7
+ import { getWorkspaceHome, isZ2hWorkspace, ensureWorkspaceNx, runYarnInstall } from '../util/workspace';
8
+
9
+ const STATE_FILE = '.z2h-migrations-state.json';
10
+
11
+ interface MigrationState {
12
+ lastMigratedVersion: string;
13
+ }
14
+
15
+ interface MigrationEntry {
16
+ version: string;
17
+ description: string;
18
+ migrate: string;
19
+ }
20
+
21
+ interface MigrationsJson {
22
+ migrations: Record<string, MigrationEntry>;
23
+ }
24
+
25
+ // ─── Helpers ───
26
+
27
+ async function readMigrationState(workspaceHome: string): Promise<MigrationState | null> {
28
+ const statePath = path.join(workspaceHome, STATE_FILE);
29
+ if (!(await pathExists(statePath))) {
30
+ return null;
31
+ }
32
+ try {
33
+ return (await readJson(statePath)) as MigrationState;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ async function writeMigrationState(workspaceHome: string, state: MigrationState): Promise<void> {
40
+ const statePath = path.join(workspaceHome, STATE_FILE);
41
+ await writeJson(statePath, state, { spaces: 2 });
42
+ }
43
+
44
+ // ─── Main ───
3
45
 
4
46
  export interface MigrateOptions {
5
47
  dryRun?: boolean;
6
- json?: boolean;
7
48
  }
8
49
 
9
50
  export async function migrateCommand(cliVersion: string, opts: MigrateOptions = {}): Promise<void> {
10
- if (opts.json) {
11
- setOutputMode('json');
51
+ const workspaceHome = getWorkspaceHome();
52
+ if (!(await isZ2hWorkspace(workspaceHome))) {
53
+ warn('no Z2H workspace found');
54
+ return;
55
+ }
56
+
57
+ const state = await readMigrationState(workspaceHome);
58
+ const lastVersion = state?.lastMigratedVersion ?? '0.0.0';
59
+
60
+ if (!semver.gt(cliVersion, lastVersion)) {
61
+ info('workspace is up to date — no migrations pending');
62
+ return;
63
+ }
64
+
65
+ info(`migrating workspace from v${lastVersion} → v${cliVersion}...`);
66
+
67
+ // migrations.json is at the package root — two dirs up from dist/migrations/
68
+ const thisDir = path.dirname(fileURLToPath(import.meta.url));
69
+ const migrationsJsonPath = path.resolve(thisDir, '..', '..', 'migrations.json');
70
+
71
+ if (!(await pathExists(migrationsJsonPath))) {
72
+ warn('migrations.json not found — skipping migrations');
73
+ return;
74
+ }
75
+
76
+ const migrationsJson = (await readJson(migrationsJsonPath)) as MigrationsJson;
77
+
78
+ // Filter to pending migrations (version > lastVersion AND <= cliVersion).
79
+ // CONVENTION: a migration's `version` field must equal the CLI release it
80
+ // ships with (not the feature branch version). This ensures the `gt` check
81
+ // always catches a migration on the exact upgrade that introduces it.
82
+ const pending = Object.entries(migrationsJson.migrations)
83
+ .filter(([, entry]) => semver.gt(entry.version, lastVersion) && semver.lte(entry.version, cliVersion))
84
+ .sort(([, a], [, b]) => semver.compare(a.version, b.version));
85
+
86
+ if (opts.dryRun) {
87
+ info(`${pending.length} pending migration(s) (dry-run — no changes will be applied):`);
88
+ for (const [, entry] of pending) {
89
+ info(` - ${entry.description} (v${entry.version})`);
90
+ }
91
+ return;
92
+ }
93
+
94
+ if (pending.length === 0) {
95
+ await writeMigrationState(workspaceHome, { lastMigratedVersion: cliVersion });
96
+ return;
97
+ }
98
+
99
+ const nxReady = await ensureWorkspaceNx(workspaceHome);
100
+ if (!nxReady) {
101
+ return;
102
+ }
103
+
104
+ info(`applying ${pending.length} workspace migration(s)...`);
105
+
106
+ // Resolve NX from the workspace's node_modules (not from CLI's own deps)
107
+ const wsRequire = createRequire(path.join(workspaceHome, 'package.json'));
108
+ const { FsTree, flushChanges } = wsRequire('nx/src/generators/tree') as {
109
+ FsTree: new (root: string, isVerbose: boolean) => import('@nx/devkit').Tree & {
110
+ listChanges(): { type: string; path: string }[];
111
+ };
112
+ flushChanges: (root: string, changes: { type: string; path: string; content?: Buffer | null }[]) => void;
113
+ };
114
+
115
+ let needsInstall = false;
116
+
117
+ for (const [name, entry] of pending) {
118
+ const tree = new FsTree(workspaceHome, false);
119
+ try {
120
+ const factoryPath = path.resolve(path.dirname(migrationsJsonPath), entry.migrate);
121
+ const mod = require(factoryPath);
122
+ const fn = mod.default || mod;
123
+ fn(tree);
124
+
125
+ const changes = tree.listChanges();
126
+ if (changes.length > 0) {
127
+ if (changes.some(c => c.path.endsWith('package.json'))) {
128
+ needsInstall = true;
129
+ }
130
+ flushChanges(workspaceHome, changes);
131
+ info(` ✓ ${entry.description}`);
132
+ } else {
133
+ info(` ○ ${entry.description} (no changes needed)`);
134
+ }
135
+ } catch (err) {
136
+ const errorMessage = `migration "${name}" failed: ${(err as Error).message}`;
137
+ errorLog(`${errorMessage}\nSkipping remaining migrations — run "z2h-cli migrate" to retry.`);
138
+ throw new Error(errorMessage);
139
+ }
140
+ }
141
+
142
+ if (needsInstall) {
143
+ await runYarnInstall(workspaceHome, 'package.json changed');
12
144
  }
13
145
 
14
- await autoMigrate(cliVersion, opts.dryRun);
146
+ await writeMigrationState(workspaceHome, { lastMigratedVersion: cliVersion });
147
+ info('workspace migrations complete.');
15
148
  }
@@ -3,7 +3,7 @@ import { stdin, stdout } from 'node:process';
3
3
  import { readJson } from 'fs-extra';
4
4
  import { resolveConsumerPaths } from '../shadow/paths';
5
5
  import { mfAppName } from '../util/app-name';
6
- import { info, output, setOutputMode } from '../util/logger';
6
+ import { info, output, getOutputMode } from '../util/logger';
7
7
  import {
8
8
  fetchAppTags,
9
9
  fetchTagCatalog,
@@ -17,7 +17,6 @@ import {
17
17
  export interface TagOptions {
18
18
  /** Repeatable `--tag` — sets the full tag set non-interactively. */
19
19
  tag?: string[];
20
- json?: boolean;
21
20
  }
22
21
 
23
22
  /** Render an app's tags as `name (Type), ...` — or `(none)`. */
@@ -100,10 +99,6 @@ export async function promptForTags(appSlug: string, catalog: CatalogEntry[], cu
100
99
  }
101
100
 
102
101
  export async function tagCommand(appArg: string | undefined, opts: TagOptions): Promise<void> {
103
- if (opts.json) {
104
- setOutputMode('json');
105
- }
106
-
107
102
  const appSlug = await resolveAppSlug(appArg);
108
103
 
109
104
  // Non-interactive write: `--tag` flags set the full set directly.
@@ -118,7 +113,7 @@ export async function tagCommand(appArg: string | undefined, opts: TagOptions):
118
113
  // Read-only listing: agents (`--json`) and any non-TTY caller get the catalog
119
114
  // + current tags without a prompt — then they set tags via `--tag`. This also
120
115
  // avoids hanging on `readline` when no human is attached to stdin.
121
- if (opts.json || !process.stdin.isTTY) {
116
+ if (getOutputMode() === 'json' || !process.stdin.isTTY) {
122
117
  output(
123
118
  `[z2h-cli] "${appSlug}"\n catalog: ${
124
119
  catalog.map(t => `${t.name} (${TAG_TYPE_LABELS[t.type]})`).join(', ') || '(none)'