@mondaydotcomorg/z2h-cli 0.15.0 → 0.15.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 (110) 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/workspace.d.ts +3 -0
  81. package/dist/esm/util/workspace.d.ts.map +1 -1
  82. package/dist/esm/util/workspace.mjs +64 -2
  83. package/dist/index.js +19 -28
  84. package/dist/util/workspace.d.ts +3 -0
  85. package/dist/util/workspace.d.ts.map +1 -1
  86. package/dist/util/workspace.js +65 -0
  87. package/package.json +1 -1
  88. package/src/commands/__tests__/doctor.test.ts +12 -5
  89. package/src/commands/build.ts +1 -6
  90. package/src/commands/clean.ts +2 -9
  91. package/src/commands/create-workspace.ts +2 -10
  92. package/src/commands/create.ts +1 -6
  93. package/src/commands/delete.ts +2 -7
  94. package/src/commands/deploy.ts +5 -10
  95. package/src/commands/dev.ts +1 -6
  96. package/src/commands/doctor.ts +3 -11
  97. package/src/commands/edit.ts +2 -10
  98. package/src/commands/generate-z2h-token.ts +1 -6
  99. package/src/commands/grant.ts +2 -16
  100. package/src/commands/migrate.ts +139 -6
  101. package/src/commands/tag.ts +2 -7
  102. package/src/index.ts +29 -41
  103. package/src/util/workspace.ts +75 -1
  104. package/dist/esm/migrations/auto-migrate.d.ts +0 -2
  105. package/dist/esm/migrations/auto-migrate.d.ts.map +0 -1
  106. package/dist/esm/migrations/auto-migrate.mjs +0 -169
  107. package/dist/migrations/auto-migrate.d.ts +0 -2
  108. package/dist/migrations/auto-migrate.d.ts.map +0 -1
  109. package/dist/migrations/auto-migrate.js +0 -177
  110. package/src/migrations/auto-migrate.ts +0 -217
@@ -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)'
package/src/index.ts CHANGED
@@ -17,7 +17,7 @@ import { migrateCommand } from './commands/migrate';
17
17
  import { tagCommand } from './commands/tag';
18
18
  import { editCommand } from './commands/edit';
19
19
  import { doctorCommand } from './commands/doctor';
20
- import { errorLog, fail, getOutputMode } from './util/logger';
20
+ import { errorLog, fail, getOutputMode, setOutputMode } from './util/logger';
21
21
  import { trackDuration } from './util/tracker';
22
22
  import { getCwdAppName } from './util/app-name';
23
23
 
@@ -69,15 +69,20 @@ async function main(): Promise<void> {
69
69
  ' 2 system error (yarn install or trident build failed)',
70
70
  ].join('\n')
71
71
  )
72
- .version(version);
72
+ .version(version)
73
+ .option('--json', 'emit structured JSON on stdout')
74
+ .hook('preAction', (_thisCommand, actionCommand) => {
75
+ if (actionCommand.optsWithGlobals<{ json?: boolean }>().json) {
76
+ setOutputMode('json');
77
+ }
78
+ });
73
79
 
74
80
  program
75
81
  .command('build')
76
82
  .description('Build the consumer into its .zth/ shadow and copy artifacts to ./build/')
77
- .option('--json', 'emit structured JSON on stdout')
78
- .action(async (opts: { json?: boolean }) => {
83
+ .action(async () => {
79
84
  try {
80
- await trackDuration('z2h_app_build_success', () => buildCommand(opts), { app_name: getCwdAppName() });
85
+ await trackDuration('z2h_app_build_success', () => buildCommand(), { app_name: getCwdAppName() });
81
86
  } catch (err) {
82
87
  await fail(`build failed: ${(err as Error).message}`, { command: 'build' }, 2);
83
88
  }
@@ -87,8 +92,7 @@ async function main(): Promise<void> {
87
92
  .command('dev')
88
93
  .description('Start the Trident dev server against .zth/ with native Vite HMR')
89
94
  .option('--open', 'open the browser automatically when the dev server is ready')
90
- .option('--json', 'emit structured JSON on stdout')
91
- .action(async (opts: { open?: boolean; json?: boolean }) => {
95
+ .action(async (opts: { open?: boolean }) => {
92
96
  try {
93
97
  await devCommand(opts);
94
98
  } catch (err) {
@@ -99,10 +103,9 @@ async function main(): Promise<void> {
99
103
  program
100
104
  .command('clean')
101
105
  .description('Delete <consumer>/.zth/ and <consumer>/build/')
102
- .option('--json', 'emit structured JSON on stdout')
103
- .action(async (opts: { json?: boolean }) => {
106
+ .action(async () => {
104
107
  try {
105
- await cleanCommand(opts);
108
+ await cleanCommand();
106
109
  } catch (err) {
107
110
  await fail(`clean failed: ${(err as Error).message}`, { command: 'clean' }, 2);
108
111
  }
@@ -112,7 +115,6 @@ async function main(): Promise<void> {
112
115
  .command('deploy')
113
116
  .description('Build, upload to S3, and register the app with the Z2H backend')
114
117
  .option('--dry-run', 'print what would happen without uploading anything')
115
- .option('--json', 'emit structured JSON on stdout')
116
118
  .option(
117
119
  '--remote <type>',
118
120
  'First deploy only: source control remote to initialise — s3 (shared storage) or github (your repo). Error if the app already has a git remote.'
@@ -121,7 +123,7 @@ async function main(): Promise<void> {
121
123
  '--repo-url <url>',
122
124
  'First deploy only: GitHub repo URL — required when --remote=github. Error if the app already has a git remote.'
123
125
  )
124
- .action(async (opts: { dryRun?: boolean; json?: boolean; remote?: string; repoUrl?: string }) => {
126
+ .action(async (opts: { dryRun?: boolean; remote?: string; repoUrl?: string }) => {
125
127
  try {
126
128
  await trackDuration(
127
129
  'z2h_app_deploy_success',
@@ -139,8 +141,7 @@ async function main(): Promise<void> {
139
141
  .command('tag [app-name]')
140
142
  .description("Set an app's category tags — app type & team (interactive unless --tag or --json is given)")
141
143
  .option('--tag <tag>', 'set a tag (repeatable); full-replaces the tag set non-interactively', collectTag, [])
142
- .option('--json', "read-only: print the popular catalog + the app's current tags as JSON (no prompt)")
143
- .action(async (appName: string | undefined, opts: { tag?: string[]; json?: boolean }) => {
144
+ .action(async (appName: string | undefined, opts: { tag?: string[] }) => {
144
145
  try {
145
146
  await trackDuration('z2h_app_tag_success', () => tagCommand(appName, opts), {
146
147
  app_name: appName ?? getCwdAppName(),
@@ -153,10 +154,9 @@ async function main(): Promise<void> {
153
154
  program
154
155
  .command('create-workspace')
155
156
  .description(`Bootstrap the ${getWorkspaceHome()}/ yarn workspace (one-time setup)`)
156
- .option('--json', 'emit structured JSON on stdout')
157
- .action(async (opts: { json?: boolean }) => {
157
+ .action(async () => {
158
158
  try {
159
- await createWorkspaceCommand(opts);
159
+ await createWorkspaceCommand();
160
160
  } catch (err) {
161
161
  await fail(`create-workspace failed: ${(err as Error).message}`, { command: 'create-workspace' }, 1);
162
162
  }
@@ -169,8 +169,7 @@ async function main(): Promise<void> {
169
169
  )
170
170
  .option('--no-browser', "don't auto-open the browser; print the URL and prompt for manual paste")
171
171
  .option('--url <url>', 'Override the token page URL (e.g. for staging)')
172
- .option('--json', 'emit structured JSON on stdout')
173
- .action(async (opts: { noBrowser?: boolean; url?: string; json?: boolean }) => {
172
+ .action(async (opts: { noBrowser?: boolean; url?: string }) => {
174
173
  try {
175
174
  await generateZ2HTokenCommand(opts);
176
175
  } catch (err) {
@@ -185,12 +184,8 @@ async function main(): Promise<void> {
185
184
  .option('--html-file <path>', 'Path to an HTML file to embed (implies --template html-embed)')
186
185
  .option('--remote <type>', 'Source control remote: s3 (shared storage) or github (your repo)')
187
186
  .option('--repo-url <url>', 'GitHub repo URL — required when --remote=github')
188
- .option('--json', 'emit structured JSON on stdout')
189
187
  .action(
190
- async (
191
- appName: string,
192
- opts: { template?: string; htmlFile?: string; remote?: string; repoUrl?: string; json?: boolean }
193
- ) => {
188
+ async (appName: string, opts: { template?: string; htmlFile?: string; remote?: string; repoUrl?: string }) => {
194
189
  try {
195
190
  await trackDuration('z2h_app_created', () => createCommand(appName, opts as CreateOptions), {
196
191
  app_name: appName,
@@ -206,10 +201,9 @@ async function main(): Promise<void> {
206
201
  program
207
202
  .command('edit <app-name>')
208
203
  .description(`Pull the latest version of an app into ${getWorkspaceHome()}/<app-name>, cloning it first if needed`)
209
- .option('--json', 'emit structured JSON on stdout')
210
- .action(async (appName: string, opts: { json?: boolean }) => {
204
+ .action(async (appName: string) => {
211
205
  try {
212
- await trackDuration('z2h_app_edit_pulled', () => editCommand(appName, opts), {
206
+ await trackDuration('z2h_app_edit_pulled', () => editCommand(appName), {
213
207
  app_name: appName,
214
208
  });
215
209
  } catch (err) {
@@ -222,8 +216,7 @@ async function main(): Promise<void> {
222
216
  .description('Grant access to the current app (email, --public, or both)')
223
217
  .option('--public', 'make the app visible to all monday.com employees')
224
218
  .option('--level <level>', 'access level for user grants: owner, editor, or viewer (default: viewer)')
225
- .option('--json', 'emit structured JSON on stdout')
226
- .action(async (email: string | undefined, opts: { public?: boolean; level?: string; json?: boolean }) => {
219
+ .action(async (email: string | undefined, opts: { public?: boolean; level?: string }) => {
227
220
  try {
228
221
  await grantCommand(email, opts as Parameters<typeof grantCommand>[1]);
229
222
  } catch (err) {
@@ -235,8 +228,7 @@ async function main(): Promise<void> {
235
228
  .command('revoke [email]')
236
229
  .description('Revoke access to the current app (email or --public; owners/admins only)')
237
230
  .option('--public', 'remove the public grant, making the app visible to owners/admins only')
238
- .option('--json', 'emit structured JSON on stdout')
239
- .action(async (email: string | undefined, opts: { public?: boolean; json?: boolean }) => {
231
+ .action(async (email: string | undefined, opts: { public?: boolean }) => {
240
232
  try {
241
233
  await revokeCommand(email, opts as Parameters<typeof revokeCommand>[1]);
242
234
  } catch (err) {
@@ -247,10 +239,9 @@ async function main(): Promise<void> {
247
239
  program
248
240
  .command('transfer-owner [email]')
249
241
  .description('Transfer ownership of the current app to another monday.com user')
250
- .option('--json', 'emit structured JSON on stdout')
251
- .action(async (email: string | undefined, opts: { json?: boolean }) => {
242
+ .action(async (email: string | undefined) => {
252
243
  try {
253
- await transferOwnerCommand(email, opts);
244
+ await transferOwnerCommand(email);
254
245
  } catch (err) {
255
246
  await fail(`transfer-owner failed: ${(err as Error).message}`, { command: 'transfer-owner' }, 1);
256
247
  }
@@ -260,8 +251,7 @@ async function main(): Promise<void> {
260
251
  .command('delete')
261
252
  .description('Permanently delete the current app and take it offline (owners/admins only)')
262
253
  .option('--yes', 'skip the confirmation prompt (required for --json / non-interactive)')
263
- .option('--json', 'emit structured JSON on stdout')
264
- .action(async (opts: { yes?: boolean; json?: boolean }) => {
254
+ .action(async (opts: { yes?: boolean }) => {
265
255
  try {
266
256
  await trackDuration('z2h_app_delete_success', () => deleteCommand(opts), {
267
257
  app_name: getCwdAppName(),
@@ -275,8 +265,7 @@ async function main(): Promise<void> {
275
265
  .command('migrate')
276
266
  .description('Run pending workspace migrations')
277
267
  .option('--dry-run', 'show pending migrations without applying any changes')
278
- .option('--json', 'emit structured JSON on stdout')
279
- .action(async (opts: { dryRun?: boolean; json?: boolean }) => {
268
+ .action(async (opts: { dryRun?: boolean }) => {
280
269
  try {
281
270
  await migrateCommand(version, opts);
282
271
  } catch (err) {
@@ -287,10 +276,9 @@ async function main(): Promise<void> {
287
276
  program
288
277
  .command('doctor')
289
278
  .description('Check that all Z2H tools, auth, workspace, and plugins are correctly set up')
290
- .option('--json', 'emit structured JSON on stdout')
291
- .action(async (opts: { json?: boolean }) => {
279
+ .action(async () => {
292
280
  try {
293
- await doctorCommand(opts);
281
+ await doctorCommand();
294
282
  } catch (err) {
295
283
  await fail(`doctor failed: ${(err as Error).message}`, { command: 'doctor' }, 1);
296
284
  }