@zoahhq/cli 0.1.8 → 0.1.9

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 (3) hide show
  1. package/README.md +25 -3
  2. package/dist/cli.mjs +366 -151
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,7 +46,7 @@ zoah login --organization acme # skip the organization list
46
46
 
47
47
  After you sign in, the CLI asks which organization to work in. Your active organization in the web app is first and selected. Use the arrow keys to choose another, then press Enter. An account with one organization skips the question.
48
48
 
49
- The CLI keeps this choice. Changing organizations in the web app does not change it. New projects from `zoah import` go into this organization. A repo that already has `.zoah/config.json` keeps updating its own project.
49
+ The CLI keeps this choice. Changing organizations in the web app does not change it. New projects from `zoah import` go into this organization, and so do directories that are imported but not swapped yet. A swapped directory stays on its project (see [Where a directory stands](#where-a-directory-stands)).
50
50
 
51
51
  The CLI uses `ZOAH_BASE_URL` when it is set. Otherwise, it uses `https://zoah.com`. `OPACITY_BASE_URL` remains a compatibility fallback.
52
52
 
@@ -59,6 +59,14 @@ By default, this removes the **active** credentials. It removes project-local cr
59
59
 
60
60
  To clear both, run logout twice.
61
61
 
62
+ ### `zoah status`
63
+
64
+ Prints who is signed in, the CLI's organization, and this directory's project, package and state, plus what the next `zoah import` will do. It reads local files only.
65
+
66
+ ```bash
67
+ zoah status
68
+ ```
69
+
62
70
  ### `zoah whoami`
63
71
 
64
72
  Prints the active user, the source credentials file, the base URL, and the organization the CLI works in. Use this when project credentials hide the global token.
@@ -215,18 +223,32 @@ zoah import # scans ./src
215
223
  zoah import src/components # scope to a subdirectory
216
224
  zoah import --swap # also rewrite local imports after publish
217
225
  zoah import --project-name my-ui # override the first project name
226
+ zoah import --new-project # start over in a new project (before a swap)
218
227
  zoah import --branch main # target a specific branch
219
228
  zoah import --debug # also write .zoah/debug/payload.json
220
229
  zoah import --print-issues warn # print issue details at or above this level
221
230
  zoah import --dry-run # preview locally without an API call
222
231
  ```
223
232
 
224
- The first import creates a project and saves its IDs to `.zoah/config.json`. Later imports use the same project.
233
+ The first import creates a project and saves its IDs to `.zoah/config.json`. Before it scans, `zoah import` says where the directory stands and where this import goes.
234
+
235
+ #### Where a directory stands
236
+
237
+ `zoah status` prints this without scanning or calling the API.
238
+
239
+ | State | What it means | What `zoah import` does |
240
+ | -------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
241
+ | New | No `.zoah/config.json`. | Creates a project in the CLI's organization. |
242
+ | Imported | Linked to a project, but no source file uses its package yet. | Updates the linked project. If the CLI's organization is different, it creates a project there and relinks the directory. `--new-project` also starts a new project. |
243
+ | Swapped | `zoah swap` rewrote component files to use the package. | Adds and updates components in the linked project, whichever organization the CLI is in. Swapped components are already in Zoah, so the scan skips them. `--new-project` is refused, because the code depends on the package. |
244
+
245
+ A swapped directory stays on its project because its code imports that project's package. It still takes new components: add one and run `zoah import --swap`.
225
246
 
226
247
  Notable flags:
227
248
 
228
249
  - `--swap`: runs `zoah swap` after a successful import.
229
- - `--project-name <name>`: sets the first project name. Later imports use the project ID in `.zoah/config.json`.
250
+ - `--project-name <name>`: sets the name of a new project. Later imports use the project ID in `.zoah/config.json`.
251
+ - `--new-project`: creates a new project in the CLI's organization and relinks the directory. Not possible once the directory is swapped.
230
252
  - `--branch <name>`: defaults to the project's saved branch (usually `main`).
231
253
  - `--base-url <url>`: overrides the URL saved at login.
232
254
  - `--print-issues <level>`: also prints full details for every issue at or above `info | warn | error`.
package/dist/cli.mjs CHANGED
@@ -422,6 +422,7 @@ var init_client = __esm({
422
422
  // src/config/local.ts
423
423
  var local_exports = {};
424
424
  __export(local_exports, {
425
+ manifestSourceFile: () => manifestSourceFile,
425
426
  readComponentsManifest: () => readComponentsManifest,
426
427
  readPackageManifest: () => readPackageManifest,
427
428
  readProjectConfig: () => readProjectConfig,
@@ -430,6 +431,11 @@ __export(local_exports, {
430
431
  writePackageManifest: () => writePackageManifest,
431
432
  writeProjectConfig: () => writeProjectConfig
432
433
  });
434
+ function manifestSourceFile(key, entry) {
435
+ if (entry.sourceFile !== void 0) return entry.sourceFile;
436
+ const hash = key.indexOf("#");
437
+ return hash === -1 ? key : key.slice(0, hash);
438
+ }
433
439
  function readProjectConfig(cwd) {
434
440
  return readCurrentOrLegacy(
435
441
  zoahPaths.config(cwd),
@@ -471,6 +477,120 @@ var init_local = __esm({
471
477
  }
472
478
  });
473
479
 
480
+ // src/config/directory-state.ts
481
+ import { existsSync } from "fs";
482
+ import { readFile as readFile2 } from "fs/promises";
483
+ import { isAbsolute, join as join2 } from "path";
484
+ function importsPackage(source, packageName) {
485
+ const escaped = packageName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
486
+ return new RegExp(`from\\s+["']${escaped}(?:/[^"']*)?["']`).test(source);
487
+ }
488
+ async function readDirectoryState(cwd) {
489
+ const config = await readProjectConfig(cwd);
490
+ if (!config) return { kind: "new" };
491
+ const manifest = await readPackageManifest(cwd);
492
+ const components2 = await readComponentsManifest(cwd);
493
+ if (!manifest || !components2) {
494
+ return { kind: "imported", config, manifest, components: components2 };
495
+ }
496
+ const files = new Set(
497
+ Object.entries(components2).map(
498
+ ([key, entry]) => manifestSourceFile(key, entry)
499
+ )
500
+ );
501
+ const swappedFiles = [];
502
+ for (const file of files) {
503
+ const path4 = isAbsolute(file) ? file : join2(cwd, file);
504
+ if (!existsSync(path4)) continue;
505
+ if (importsPackage(await readFile2(path4, "utf8"), manifest.name)) {
506
+ swappedFiles.push(file);
507
+ }
508
+ }
509
+ if (swappedFiles.length === 0) {
510
+ return { kind: "imported", config, manifest, components: components2 };
511
+ }
512
+ return { kind: "swapped", config, manifest, components: components2, swappedFiles };
513
+ }
514
+ function resolveImportTarget(state, organization, opts = {}) {
515
+ if (state.kind === "new") return { kind: "new", reason: "first" };
516
+ if (state.kind === "swapped") {
517
+ if (opts.newProject) {
518
+ throw new DirectoryLockedError(state, "Starting a new project");
519
+ }
520
+ return {
521
+ kind: "existing",
522
+ config: state.config,
523
+ components: state.components,
524
+ locked: true
525
+ };
526
+ }
527
+ if (opts.newProject) {
528
+ return { kind: "new", reason: "requested", replaces: state.config };
529
+ }
530
+ if (organization && organization.id !== state.config.orgId) {
531
+ return { kind: "new", reason: "organization", replaces: state.config };
532
+ }
533
+ return {
534
+ kind: "existing",
535
+ config: state.config,
536
+ components: state.components,
537
+ locked: false
538
+ };
539
+ }
540
+ function projectLabel(config) {
541
+ return `${config.projectSlug ?? config.projectId} in ${config.orgSlug ?? config.orgId}`;
542
+ }
543
+ function organizationLabel(organization) {
544
+ if (!organization) return "your active organization";
545
+ return organization.slug ? `${organization.name} (${organization.slug})` : organization.name;
546
+ }
547
+ function describeState(state) {
548
+ switch (state.kind) {
549
+ case "new":
550
+ return "Not linked to a Zoah project yet.";
551
+ case "imported":
552
+ return `Linked to ${projectLabel(state.config)}. Nothing is swapped yet, so the link can still change.`;
553
+ case "swapped":
554
+ return `Swapped to ${state.manifest.name} (${state.swappedFiles.length === 1 ? "1 file" : `${state.swappedFiles.length} files`}), so it stays on ${projectLabel(state.config)}.`;
555
+ }
556
+ }
557
+ function describeTarget(target, organization) {
558
+ if (target.kind === "existing") {
559
+ return `Adds and updates components in ${projectLabel(target.config)}.`;
560
+ }
561
+ const where = organizationLabel(organization);
562
+ switch (target.reason) {
563
+ case "first":
564
+ return `Creates a project in ${where}.`;
565
+ case "organization":
566
+ return `Creates a project in ${where}, the CLI's organization, and links this directory to it instead of ${projectLabel(target.replaces)}.`;
567
+ case "requested":
568
+ return `Creates a new project in ${where} and links this directory to it instead of ${projectLabel(target.replaces)}.`;
569
+ }
570
+ }
571
+ function organizationNote(target, organization) {
572
+ if (target.kind === "existing" && target.locked && organization && organization.id !== target.config.orgId) {
573
+ return `The CLI works in ${organizationLabel(organization)}, but a swapped directory stays on its project.`;
574
+ }
575
+ return null;
576
+ }
577
+ var DirectoryLockedError;
578
+ var init_directory_state = __esm({
579
+ "src/config/directory-state.ts"() {
580
+ "use strict";
581
+ init_local();
582
+ DirectoryLockedError = class extends Error {
583
+ constructor(state, action) {
584
+ super(
585
+ `${action} is not possible here: this directory is swapped to ${state.manifest.name} (${state.swappedFiles.length === 1 ? "1 file" : `${state.swappedFiles.length} files`}), so it stays on ${projectLabel(state.config)}. \`zoah import\` still adds and updates components in that project.`
586
+ );
587
+ this.state = state;
588
+ this.name = "DirectoryLockedError";
589
+ }
590
+ };
591
+ }
592
+ });
593
+
474
594
  // src/mcp/refs.ts
475
595
  function matchRef(options) {
476
596
  const trimmed = options.reference.trim();
@@ -1074,7 +1194,7 @@ var CLI_VERSION;
1074
1194
  var init_version = __esm({
1075
1195
  "src/config/version.ts"() {
1076
1196
  "use strict";
1077
- CLI_VERSION = true ? "0.1.8" : "0.0.0-dev";
1197
+ CLI_VERSION = true ? "0.1.9" : "0.0.0-dev";
1078
1198
  }
1079
1199
  });
1080
1200
 
@@ -1179,7 +1299,9 @@ function StatusLine({ children }) {
1179
1299
  }
1180
1300
  function introCopy({
1181
1301
  userEmail,
1182
- zoahUrl
1302
+ zoahUrl,
1303
+ directory,
1304
+ next
1183
1305
  }) {
1184
1306
  if (!userEmail) {
1185
1307
  return {
@@ -1197,7 +1319,7 @@ function introCopy({
1197
1319
  }
1198
1320
  return {
1199
1321
  heading: "Want to rescan?",
1200
- body: "We will scan your library again and upload it as a new Zoah project. The previous project stays available.",
1322
+ body: directory && next ? `${directory} The next import: ${next}` : "We will scan your library again and update the linked Zoah project.",
1201
1323
  goLabel: "Rescan"
1202
1324
  };
1203
1325
  }
@@ -1221,12 +1343,14 @@ var init_screens = __esm({
1221
1343
  userEmail,
1222
1344
  zoahUrl,
1223
1345
  lastScanAge,
1224
- sync: sync2
1346
+ sync: sync2,
1347
+ directory,
1348
+ next
1225
1349
  }) => function Intro({ value, onFinish }) {
1226
1350
  const settled = value !== void 0;
1227
1351
  const screenReaderEnabled = useIsScreenReaderEnabled();
1228
1352
  const [logoDone, setLogoDone] = useState2(settled || screenReaderEnabled);
1229
- const copy = introCopy({ userEmail, zoahUrl });
1353
+ const copy = introCopy({ userEmail, zoahUrl, directory, next });
1230
1354
  const showStatus = useReveal(150, logoDone);
1231
1355
  const showPitch = useReveal(500, logoDone);
1232
1356
  const showPicker = useReveal(850, logoDone);
@@ -1363,6 +1487,9 @@ var init_screens = __esm({
1363
1487
  };
1364
1488
  COMPONENT_LIST_CAP = 20;
1365
1489
  DesignSummary = (report) => {
1490
+ if (report.upToDate) {
1491
+ return /* @__PURE__ */ jsx3(Panel, { title: "Nothing new to import", children: /* @__PURE__ */ jsx3(Text3, { children: `The components here are already swapped to ${report.upToDate}. Add a component, then run \`zoah\` again.` }) });
1492
+ }
1366
1493
  const checkRow = (key, label, suffix) => /* @__PURE__ */ jsxs3(Box3, { gap: 1, children: [
1367
1494
  /* @__PURE__ */ jsx3(Text3, { color: "green", children: "\u2713" }),
1368
1495
  /* @__PURE__ */ jsx3(Text3, { children: label }),
@@ -1554,17 +1681,12 @@ async function runOrganizationSwitch(opts) {
1554
1681
  const organization = toCliOrganization(chosen);
1555
1682
  await writeCredentialOrganization(found, organization);
1556
1683
  console.log(`The CLI now works in ${describe(organization)}.`);
1557
- const config = await readProjectConfig(opts.cwd);
1558
- if (config && config.orgId !== organization.id) {
1684
+ const state = await readDirectoryState(opts.cwd);
1685
+ if (state.kind !== "new") {
1686
+ const target = resolveImportTarget(state, organization);
1687
+ console.log(dim(` This directory: ${describeState(state)}`));
1559
1688
  console.log(
1560
- dim(
1561
- ` This repo's project is in ${config.orgSlug ?? config.orgId}, and \`zoah import\` keeps updating it.`
1562
- )
1563
- );
1564
- console.log(
1565
- dim(
1566
- " To import into this organization instead, remove .zoah/config.json, .zoah/components.json and .zoah/manifest.json."
1567
- )
1689
+ dim(` Next import here: ${describeTarget(target, organization)}`)
1568
1690
  );
1569
1691
  }
1570
1692
  }
@@ -1586,7 +1708,7 @@ var init_organization = __esm({
1586
1708
  "use strict";
1587
1709
  init_client();
1588
1710
  init_session();
1589
- init_local();
1711
+ init_directory_state();
1590
1712
  init_refs();
1591
1713
  init_colors();
1592
1714
  }
@@ -1673,11 +1795,11 @@ var init_errors = __esm({
1673
1795
  });
1674
1796
 
1675
1797
  // src/util/ensure-gitignore.ts
1676
- import { readFile as readFile2 } from "fs/promises";
1798
+ import { readFile as readFile3 } from "fs/promises";
1677
1799
  async function ensureZoahGitignore(cwd) {
1678
1800
  const path4 = zoahPaths.gitignore(cwd);
1679
1801
  try {
1680
- const current2 = await readFile2(path4, "utf8");
1802
+ const current2 = await readFile3(path4, "utf8");
1681
1803
  if (current2 === GITIGNORE_CONTENT) return;
1682
1804
  } catch (err) {
1683
1805
  if (err.code !== "ENOENT") throw err;
@@ -25557,14 +25679,14 @@ var init_issues_snapshot = __esm({
25557
25679
  });
25558
25680
 
25559
25681
  // src/parse/scan.ts
25560
- import { existsSync, statSync } from "fs";
25561
- import { isAbsolute, join as join2, relative, resolve } from "path";
25682
+ import { existsSync as existsSync2, statSync } from "fs";
25683
+ import { isAbsolute as isAbsolute2, join as join3, relative, resolve } from "path";
25562
25684
  import { Project } from "ts-morph";
25563
25685
  function findTsConfig(cwd) {
25564
25686
  const candidates = ["tsconfig.json", "jsconfig.json"];
25565
25687
  for (const name of candidates) {
25566
- const full = join2(cwd, name);
25567
- if (existsSync(full)) return full;
25688
+ const full = join3(cwd, name);
25689
+ if (existsSync2(full)) return full;
25568
25690
  }
25569
25691
  return null;
25570
25692
  }
@@ -25580,17 +25702,17 @@ function loadProject(opts) {
25580
25702
  /* Preserve */
25581
25703
  }
25582
25704
  });
25583
- const scopeAbsolute = isAbsolute(opts.scopePath) ? opts.scopePath : resolve(opts.cwd, opts.scopePath);
25584
- if (existsSync(scopeAbsolute)) {
25705
+ const scopeAbsolute = isAbsolute2(opts.scopePath) ? opts.scopePath : resolve(opts.cwd, opts.scopePath);
25706
+ if (existsSync2(scopeAbsolute)) {
25585
25707
  const stat = statSync(scopeAbsolute);
25586
25708
  if (stat.isDirectory()) {
25587
25709
  const patterns = SOURCE_EXTENSIONS.map(
25588
- (ext) => join2(scopeAbsolute, `**/*.${ext}`)
25710
+ (ext) => join3(scopeAbsolute, `**/*.${ext}`)
25589
25711
  );
25590
25712
  for (const dir of IGNORED_DIRS) {
25591
- patterns.push(`!${join2(scopeAbsolute, `**/${dir}/**`)}`);
25713
+ patterns.push(`!${join3(scopeAbsolute, `**/${dir}/**`)}`);
25592
25714
  }
25593
- patterns.push(`!${join2(scopeAbsolute, "**/*.d.ts")}`);
25715
+ patterns.push(`!${join3(scopeAbsolute, "**/*.d.ts")}`);
25594
25716
  project.addSourceFilesAtPaths(patterns);
25595
25717
  } else {
25596
25718
  project.addSourceFileAtPathIfExists(scopeAbsolute);
@@ -25740,7 +25862,7 @@ var init_classify = __esm({
25740
25862
  });
25741
25863
 
25742
25864
  // src/parse/entry-points.ts
25743
- import { existsSync as existsSync2 } from "fs";
25865
+ import { existsSync as existsSync3 } from "fs";
25744
25866
  import { resolve as resolve2 } from "path";
25745
25867
  function isFrameworkReservedFile(filePath) {
25746
25868
  const base = filePath.split("/").pop() ?? "";
@@ -25752,7 +25874,7 @@ function findEntrySkips(project, cwd) {
25752
25874
  const entryFiles = [];
25753
25875
  for (const candidate of ENTRY_CANDIDATES) {
25754
25876
  const fullPath = resolve2(cwd, candidate);
25755
- if (!existsSync2(fullPath)) continue;
25877
+ if (!existsSync3(fullPath)) continue;
25756
25878
  let sf = project.getSourceFile(fullPath);
25757
25879
  if (!sf) {
25758
25880
  sf = project.addSourceFileAtPathIfExists(fullPath) ?? void 0;
@@ -25885,10 +26007,10 @@ var init_styled_components = __esm({
25885
26007
  });
25886
26008
 
25887
26009
  // src/parse/module-resolution.ts
25888
- import { existsSync as existsSync3, readFileSync, readdirSync, statSync as statSync2 } from "fs";
25889
- import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve3, sep } from "path";
26010
+ import { existsSync as existsSync4, readFileSync, readdirSync, statSync as statSync2 } from "fs";
26011
+ import { dirname as dirname2, isAbsolute as isAbsolute3, join as join4, relative as relative2, resolve as resolve3, sep } from "path";
25890
26012
  function readTsconfig(absPath) {
25891
- if (!existsSync3(absPath)) return null;
26013
+ if (!existsSync4(absPath)) return null;
25892
26014
  let raw;
25893
26015
  try {
25894
26016
  raw = readFileSync(absPath, "utf8");
@@ -25906,7 +26028,7 @@ function readTsconfig(absPath) {
25906
26028
  function loadTsconfigPaths(cwd) {
25907
26029
  const candidates = ["tsconfig.json", "jsconfig.json"];
25908
26030
  for (const name of candidates) {
25909
- const path4 = join3(cwd, name);
26031
+ const path4 = join4(cwd, name);
25910
26032
  const tsconfig = readTsconfig(path4);
25911
26033
  if (!tsconfig) continue;
25912
26034
  const opts = tsconfig.compilerOptions;
@@ -25956,13 +26078,13 @@ function expandWorkspaceGlob(rootCwd, pattern) {
25956
26078
  const starIdx = cleaned.indexOf("*");
25957
26079
  if (starIdx < 0) {
25958
26080
  const abs = resolve3(rootCwd, cleaned);
25959
- return existsSync3(join3(abs, "package.json")) ? [abs] : [];
26081
+ return existsSync4(join4(abs, "package.json")) ? [abs] : [];
25960
26082
  }
25961
26083
  const prefix = cleaned.slice(0, starIdx).replace(/\/$/, "");
25962
26084
  const suffix = cleaned.slice(starIdx).replace(/^\*+\/?/, "");
25963
26085
  if (suffix.length > 0) return [];
25964
26086
  const dir = resolve3(rootCwd, prefix);
25965
- if (!existsSync3(dir)) return [];
26087
+ if (!existsSync4(dir)) return [];
25966
26088
  let entries;
25967
26089
  try {
25968
26090
  entries = readdirSync(dir);
@@ -25972,19 +26094,19 @@ function expandWorkspaceGlob(rootCwd, pattern) {
25972
26094
  const out = [];
25973
26095
  for (const entry of entries) {
25974
26096
  if (entry.startsWith(".")) continue;
25975
- const child = join3(dir, entry);
26097
+ const child = join4(dir, entry);
25976
26098
  try {
25977
26099
  if (!statSync2(child).isDirectory()) continue;
25978
26100
  } catch {
25979
26101
  continue;
25980
26102
  }
25981
- if (existsSync3(join3(child, "package.json"))) out.push(child);
26103
+ if (existsSync4(join4(child, "package.json"))) out.push(child);
25982
26104
  }
25983
26105
  return out;
25984
26106
  }
25985
26107
  function readPackageJson(packageRoot) {
25986
- const path4 = join3(packageRoot, "package.json");
25987
- if (!existsSync3(path4)) return null;
26108
+ const path4 = join4(packageRoot, "package.json");
26109
+ if (!existsSync4(path4)) return null;
25988
26110
  try {
25989
26111
  const parsed = JSON.parse(readFileSync(path4, "utf8"));
25990
26112
  if (parsed && typeof parsed === "object") {
@@ -25996,8 +26118,8 @@ function readPackageJson(packageRoot) {
25996
26118
  }
25997
26119
  }
25998
26120
  function readPnpmWorkspaceGlobs(cwd) {
25999
- const path4 = join3(cwd, "pnpm-workspace.yaml");
26000
- if (!existsSync3(path4)) return [];
26121
+ const path4 = join4(cwd, "pnpm-workspace.yaml");
26122
+ if (!existsSync4(path4)) return [];
26001
26123
  let raw;
26002
26124
  try {
26003
26125
  raw = readFileSync(path4, "utf8");
@@ -26050,8 +26172,8 @@ function loadWorkspacePackages(cwd) {
26050
26172
  });
26051
26173
  }
26052
26174
  }
26053
- const lernaPath = join3(cwd, "lerna.json");
26054
- if (existsSync3(lernaPath)) {
26175
+ const lernaPath = join4(cwd, "lerna.json");
26176
+ if (existsSync4(lernaPath)) {
26055
26177
  try {
26056
26178
  const lerna = JSON.parse(readFileSync(lernaPath, "utf8"));
26057
26179
  if (Array.isArray(lerna.packages)) {
@@ -26072,7 +26194,7 @@ function findOwningWorkspace(workspaces, absPath) {
26072
26194
  let best = null;
26073
26195
  for (const ws of workspaces) {
26074
26196
  const rel = relative2(ws.root, absPath);
26075
- if (rel.startsWith("..") || isAbsolute2(rel)) continue;
26197
+ if (rel.startsWith("..") || isAbsolute3(rel)) continue;
26076
26198
  if (!best || ws.root.length > best.root.length) best = ws;
26077
26199
  }
26078
26200
  return best;
@@ -26177,8 +26299,8 @@ function detectBuildTimeTransforms(cwd) {
26177
26299
  const detected = [];
26178
26300
  const seen = /* @__PURE__ */ new Set();
26179
26301
  for (const candidate of TRANSFORM_CONFIG_CANDIDATES) {
26180
- const path4 = join3(cwd, candidate);
26181
- if (!existsSync3(path4)) continue;
26302
+ const path4 = join4(cwd, candidate);
26303
+ if (!existsSync4(path4)) continue;
26182
26304
  let raw;
26183
26305
  try {
26184
26306
  raw = readFileSync(path4, "utf8");
@@ -26206,7 +26328,7 @@ function buildModuleResolutionContext(cwd) {
26206
26328
  function findOwningPackageRoot(absPath) {
26207
26329
  let dir = dirname2(absPath);
26208
26330
  for (let i3 = 0; i3 < MAX_PACKAGE_WALK_DEPTH; i3++) {
26209
- if (existsSync3(join3(dir, "package.json"))) return dir;
26331
+ if (existsSync4(join4(dir, "package.json"))) return dir;
26210
26332
  const parent = dirname2(dir);
26211
26333
  if (parent === dir) return null;
26212
26334
  dir = parent;
@@ -26221,7 +26343,7 @@ function resolveImport(ctx, sourceFile, decl, issueCtx) {
26221
26343
  const line = decl.getStartLineNumber();
26222
26344
  const resolvedSf = decl.getModuleSpecifierSourceFile();
26223
26345
  const resolvedAbs = resolvedSf?.getFilePath() ?? null;
26224
- const isBareSpecifier = !specifier.startsWith(".") && !isAbsolute2(specifier);
26346
+ const isBareSpecifier = !specifier.startsWith(".") && !isAbsolute3(specifier);
26225
26347
  const tsconfigMatch = isBareSpecifier && ctx.tsconfigPaths ? matchTsconfigPath(ctx.tsconfigPaths, specifier) : null;
26226
26348
  if (tsconfigMatch) {
26227
26349
  const resolvedTo = resolvedAbs ? relative2(ctx.cwd, resolvedAbs) || resolvedAbs : tsconfigMatch.resolvedTargets[0] ? relative2(ctx.cwd, tsconfigMatch.resolvedTargets[0]) || tsconfigMatch.resolvedTargets[0] : tsconfigMatch.pattern;
@@ -26757,16 +26879,16 @@ var init_discover = __esm({
26757
26879
  });
26758
26880
 
26759
26881
  // src/parse/tailwind-tokens.ts
26760
- import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
26761
- import { join as join4 } from "path";
26882
+ import { existsSync as existsSync5, readFileSync as readFileSync2 } from "fs";
26883
+ import { join as join5 } from "path";
26762
26884
  function cssImportsTailwind(cssText) {
26763
26885
  return /@import\s+["']tailwindcss["']/.test(cssText);
26764
26886
  }
26765
26887
  function findThemeCssFiles(cwd) {
26766
26888
  const found = [];
26767
26889
  for (const rel of CSS_ENTRY_CANDIDATES) {
26768
- const p4 = join4(cwd, rel);
26769
- if (!existsSync4(p4)) continue;
26890
+ const p4 = join5(cwd, rel);
26891
+ if (!existsSync5(p4)) continue;
26770
26892
  const text = safeRead(p4);
26771
26893
  if (text && (cssImportsTailwind(text) || /:root\s*\{|@theme\b/.test(text))) {
26772
26894
  found.push(p4);
@@ -27161,8 +27283,8 @@ var init_tailwind_engine = __esm({
27161
27283
  });
27162
27284
 
27163
27285
  // src/parse/tailwind.ts
27164
- import { existsSync as existsSync5 } from "fs";
27165
- import { join as join5 } from "path";
27286
+ import { existsSync as existsSync6 } from "fs";
27287
+ import { join as join6 } from "path";
27166
27288
  import { createJiti } from "jiti";
27167
27289
  function findConfig(cwd) {
27168
27290
  const candidates = [
@@ -27172,8 +27294,8 @@ function findConfig(cwd) {
27172
27294
  "tailwind.config.mjs"
27173
27295
  ];
27174
27296
  for (const name of candidates) {
27175
- const p4 = join5(cwd, name);
27176
- if (existsSync5(p4)) return p4;
27297
+ const p4 = join6(cwd, name);
27298
+ if (existsSync6(p4)) return p4;
27177
27299
  }
27178
27300
  return null;
27179
27301
  }
@@ -29198,15 +29320,15 @@ var init_plain_css = __esm({
29198
29320
  });
29199
29321
 
29200
29322
  // src/parse/css-resolution.ts
29201
- import { dirname as dirname4, isAbsolute as isAbsolute3, resolve as resolve4 } from "path";
29202
- import { existsSync as existsSync6 } from "fs";
29323
+ import { dirname as dirname4, isAbsolute as isAbsolute4, resolve as resolve4 } from "path";
29324
+ import { existsSync as existsSync7 } from "fs";
29203
29325
  import {
29204
29326
  Node as Node8
29205
29327
  } from "ts-morph";
29206
29328
  function resolveImportPath(fromFile, specifier) {
29207
- if (!specifier.startsWith(".") && !isAbsolute3(specifier)) return null;
29208
- const base = isAbsolute3(specifier) ? specifier : resolve4(dirname4(fromFile), specifier);
29209
- if (existsSync6(base)) return base;
29329
+ if (!specifier.startsWith(".") && !isAbsolute4(specifier)) return null;
29330
+ const base = isAbsolute4(specifier) ? specifier : resolve4(dirname4(fromFile), specifier);
29331
+ if (existsSync7(base)) return base;
29210
29332
  return null;
29211
29333
  }
29212
29334
  function buildSourceFileResolverContext(sourceFile, emit, viewportWidth = 0) {
@@ -89897,8 +90019,8 @@ var init_dist4 = __esm({
89897
90019
  });
89898
90020
 
89899
90021
  // src/codemod/wrap-with-zoah.ts
89900
- import { existsSync as existsSync7 } from "fs";
89901
- import { join as join6, resolve as resolve5 } from "path";
90022
+ import { existsSync as existsSync8 } from "fs";
90023
+ import { join as join7, resolve as resolve5 } from "path";
89902
90024
  import {
89903
90025
  Node as Node10,
89904
90026
  Project as Project3,
@@ -89906,8 +90028,8 @@ import {
89906
90028
  } from "ts-morph";
89907
90029
  function findTsConfig2(cwd) {
89908
90030
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
89909
- const full = join6(cwd, name);
89910
- if (existsSync7(full)) return full;
90031
+ const full = join7(cwd, name);
90032
+ if (existsSync8(full)) return full;
89911
90033
  }
89912
90034
  return null;
89913
90035
  }
@@ -90817,12 +90939,9 @@ function wrapSingleFile(file, filePath, entry, packageName, rootForwardingSurfac
90817
90939
  ensureAliasedImport(file, exportName, aliasName, moduleSpecifier);
90818
90940
  return { kind: "wrapped", infoFindings };
90819
90941
  }
90820
- function sourceFileForManifestEntry(key, entry) {
90821
- return entry.sourceFile ?? key.split("#")[0] ?? key;
90822
- }
90823
90942
  function groupManifestEntries(components2) {
90824
90943
  return Object.entries(components2).map(([key, entry]) => ({
90825
- filePath: sourceFileForManifestEntry(key, entry),
90944
+ filePath: manifestSourceFile(key, entry),
90826
90945
  entry
90827
90946
  }));
90828
90947
  }
@@ -90847,7 +90966,7 @@ async function wrapWithZoah(opts) {
90847
90966
  }
90848
90967
  for (const [relPath, entries] of fileEntries) {
90849
90968
  const absPath = resolve5(opts.cwd, relPath);
90850
- if (!existsSync7(absPath)) {
90969
+ if (!existsSync8(absPath)) {
90851
90970
  result.issues.push(
90852
90971
  makeIssue({
90853
90972
  code: "CODEMOD_SOURCE_FILE_NOT_FOUND",
@@ -90980,6 +91099,7 @@ var init_wrap_with_zoah = __esm({
90980
91099
  "src/codemod/wrap-with-zoah.ts"() {
90981
91100
  "use strict";
90982
91101
  init_cli_import();
91102
+ init_local();
90983
91103
  init_discover();
90984
91104
  init_jsx_peel();
90985
91105
  init_props();
@@ -90988,17 +91108,17 @@ var init_wrap_with_zoah = __esm({
90988
91108
  });
90989
91109
 
90990
91110
  // src/codemod/install-package.ts
90991
- import { existsSync as existsSync8 } from "fs";
90992
- import { chmod as chmod2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
90993
- import { dirname as dirname5, join as join7 } from "path";
91111
+ import { existsSync as existsSync9 } from "fs";
91112
+ import { chmod as chmod2, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
91113
+ import { dirname as dirname5, join as join8 } from "path";
90994
91114
  import { homedir as homedir2 } from "os";
90995
91115
  import { spawn } from "child_process";
90996
91116
  import { request as request3 } from "undici";
90997
91117
  function detectPackageManager(cwd) {
90998
91118
  let dir = cwd;
90999
91119
  while (true) {
91000
- if (existsSync8(join7(dir, "pnpm-lock.yaml"))) return "pnpm";
91001
- if (existsSync8(join7(dir, "yarn.lock"))) return "yarn";
91120
+ if (existsSync9(join8(dir, "pnpm-lock.yaml"))) return "pnpm";
91121
+ if (existsSync9(join8(dir, "yarn.lock"))) return "yarn";
91002
91122
  const parent = dirname5(dir);
91003
91123
  if (parent === dir) break;
91004
91124
  dir = parent;
@@ -91006,11 +91126,11 @@ function detectPackageManager(cwd) {
91006
91126
  return "npm";
91007
91127
  }
91008
91128
  async function ensureNpmrcRegistry(cwd, registryUrl) {
91009
- const npmrcPath = join7(cwd, ".npmrc");
91129
+ const npmrcPath = join8(cwd, ".npmrc");
91010
91130
  const desired = `@zoah:registry=${registryUrl}`;
91011
91131
  let existing = "";
91012
- if (existsSync8(npmrcPath)) {
91013
- existing = await readFile3(npmrcPath, "utf8");
91132
+ if (existsSync9(npmrcPath)) {
91133
+ existing = await readFile4(npmrcPath, "utf8");
91014
91134
  }
91015
91135
  const lines = existing.split(/\r?\n/);
91016
91136
  const idx = lines.findIndex((l) => l.trim().startsWith("@zoah:registry="));
@@ -91029,9 +91149,9 @@ function registryHostOf(registryUrl) {
91029
91149
  }
91030
91150
  async function readNpmrcAuthToken(cwd, registryUrl) {
91031
91151
  const prefix = `//${registryHostOf(registryUrl)}/:_authToken=`;
91032
- for (const npmrcPath of [join7(cwd, ".npmrc"), join7(homedir2(), ".npmrc")]) {
91033
- if (!existsSync8(npmrcPath)) continue;
91034
- const content = await readFile3(npmrcPath, "utf8");
91152
+ for (const npmrcPath of [join8(cwd, ".npmrc"), join8(homedir2(), ".npmrc")]) {
91153
+ if (!existsSync9(npmrcPath)) continue;
91154
+ const content = await readFile4(npmrcPath, "utf8");
91035
91155
  const line = content.split(/\r?\n/).map((l) => l.trim()).find((l) => l.startsWith(prefix));
91036
91156
  if (line) return line.slice(prefix.length);
91037
91157
  }
@@ -91066,12 +91186,12 @@ function isNpmrcTracked(cwd) {
91066
91186
  });
91067
91187
  }
91068
91188
  async function ensureNpmrcAuthToken(cwd, registryUrl, token) {
91069
- const npmrcPath = join7(cwd, ".npmrc");
91189
+ const npmrcPath = join8(cwd, ".npmrc");
91070
91190
  const host = registryHostOf(registryUrl);
91071
91191
  const desired = `//${host}/:_authToken=${token}`;
91072
91192
  let existing = "";
91073
- if (existsSync8(npmrcPath)) {
91074
- existing = await readFile3(npmrcPath, "utf8");
91193
+ if (existsSync9(npmrcPath)) {
91194
+ existing = await readFile4(npmrcPath, "utf8");
91075
91195
  }
91076
91196
  const lines = existing.split(/\r?\n/);
91077
91197
  const idx = lines.findIndex(
@@ -91092,10 +91212,10 @@ async function ensureNpmrcAuthToken(cwd, registryUrl, token) {
91092
91212
  await chmod2(npmrcPath, 384);
91093
91213
  }
91094
91214
  async function ensureGitignoreHasNpmrc(cwd) {
91095
- const gitignorePath = join7(cwd, ".gitignore");
91215
+ const gitignorePath = join8(cwd, ".gitignore");
91096
91216
  let existing = "";
91097
- if (existsSync8(gitignorePath)) {
91098
- existing = await readFile3(gitignorePath, "utf8");
91217
+ if (existsSync9(gitignorePath)) {
91218
+ existing = await readFile4(gitignorePath, "utf8");
91099
91219
  const has = existing.split(/\r?\n/).some((l) => l.trim() === ".npmrc" || l.trim() === "/.npmrc");
91100
91220
  if (has) return false;
91101
91221
  }
@@ -91105,11 +91225,11 @@ async function ensureGitignoreHasNpmrc(cwd) {
91105
91225
  return true;
91106
91226
  }
91107
91227
  async function addDependencyToPackageJson(cwd, packageName, version) {
91108
- const pkgPath = join7(cwd, "package.json");
91109
- if (!existsSync8(pkgPath)) {
91228
+ const pkgPath = join8(cwd, "package.json");
91229
+ if (!existsSync9(pkgPath)) {
91110
91230
  throw new Error("No package.json found at " + cwd);
91111
91231
  }
91112
- const raw = await readFile3(pkgPath, "utf8");
91232
+ const raw = await readFile4(pkgPath, "utf8");
91113
91233
  const pkg = JSON.parse(raw);
91114
91234
  const deps = pkg.dependencies ?? {};
91115
91235
  const desired = `^${version}`;
@@ -91239,7 +91359,7 @@ async function runSwapCore(opts) {
91239
91359
  const swapped = [];
91240
91360
  const skipped = [];
91241
91361
  for (const [manifestKey, comp] of Object.entries(components2)) {
91242
- const filePath = comp.sourceFile ?? manifestKey.split("#")[0];
91362
+ const filePath = manifestSourceFile(manifestKey, comp);
91243
91363
  if (changedFiles.has(filePath)) {
91244
91364
  swapped.push(comp.packageExport);
91245
91365
  } else {
@@ -91394,18 +91514,19 @@ var init_swap = __esm({
91394
91514
  // src/commands/import.ts
91395
91515
  var import_exports = {};
91396
91516
  __export(import_exports, {
91517
+ buildDesignUrl: () => buildDesignUrl,
91397
91518
  runImport: () => runImport,
91398
91519
  runImportLocalPipeline: () => runImportLocalPipeline,
91399
91520
  runImportSubmit: () => runImportSubmit
91400
91521
  });
91401
- import { readFile as readFile4 } from "fs/promises";
91402
- import { existsSync as existsSync9 } from "fs";
91403
- import { isAbsolute as isAbsolute4, join as join8, relative as relative5 } from "path";
91522
+ import { readFile as readFile5 } from "fs/promises";
91523
+ import { existsSync as existsSync10 } from "fs";
91524
+ import { isAbsolute as isAbsolute5, join as join9, relative as relative5 } from "path";
91404
91525
  async function readPackageJsonName(cwd) {
91405
- const path4 = join8(cwd, "package.json");
91406
- if (!existsSync9(path4)) return null;
91526
+ const path4 = join9(cwd, "package.json");
91527
+ if (!existsSync10(path4)) return null;
91407
91528
  try {
91408
- const raw = await readFile4(path4, "utf8");
91529
+ const raw = await readFile5(path4, "utf8");
91409
91530
  const parsed = JSON.parse(raw);
91410
91531
  return parsed.name ?? null;
91411
91532
  } catch {
@@ -91504,14 +91625,11 @@ function componentIdsForReimport(components2, manifest, cwd) {
91504
91625
  return [...ids];
91505
91626
  }
91506
91627
  function manifestKeyForPath(cwd, filePath) {
91507
- return isAbsolute4(filePath) ? relative5(cwd, filePath) : filePath;
91628
+ return isAbsolute5(filePath) ? relative5(cwd, filePath) : filePath;
91508
91629
  }
91509
91630
  function manifestKeyForComponent(cwd, filePath, packageExport) {
91510
91631
  return `${manifestKeyForPath(cwd, filePath)}#${packageExport}`;
91511
91632
  }
91512
- function manifestSourceFile(key, entry) {
91513
- return entry.sourceFile ?? key.split("#")[0] ?? key;
91514
- }
91515
91633
  function findManifestEntryForComponent(manifest, cwd, filePath, packageExport) {
91516
91634
  const sourceFile = manifestKeyForPath(cwd, filePath);
91517
91635
  const keyed = manifest[`${sourceFile}#${packageExport}`];
@@ -91528,8 +91646,12 @@ async function runImportLocalPipeline(opts) {
91528
91646
  await ensureZoahGitignore(opts.cwd);
91529
91647
  const baseUrl = opts.baseUrl ?? creds.baseUrl;
91530
91648
  const branchName = opts.branch ?? process.env.ZOAH_BRANCH ?? process.env.OPACITY_BRANCH ?? null;
91531
- const existingConfig = await readProjectConfig(opts.cwd);
91532
- const existingComponentsManifest = existingConfig ? await readComponentsManifest(opts.cwd) : null;
91649
+ const state = await readDirectoryState(opts.cwd);
91650
+ const target = resolveImportTarget(state, creds.organization, {
91651
+ newProject: opts.newProject
91652
+ });
91653
+ const existingConfig = target.kind === "existing" ? target.config : null;
91654
+ const existingComponentsManifest = target.kind === "existing" ? target.components : null;
91533
91655
  const api = ApiClient.fromCredentials(creds, baseUrl);
91534
91656
  const commitSha = getCommitSha(opts.cwd);
91535
91657
  const issueCtx = createIssueContext({ commitSha });
@@ -91547,7 +91669,22 @@ async function runImportLocalPipeline(opts) {
91547
91669
  const entrySkip = findEntrySkips(scan2.project, opts.cwd);
91548
91670
  const preBuildIssues = [];
91549
91671
  const relPath = (abs) => relative5(opts.cwd, abs);
91672
+ const swappedExports = /* @__PURE__ */ new Set();
91673
+ if (state.kind === "swapped") {
91674
+ const swappedFiles = new Set(state.swappedFiles);
91675
+ for (const [key, entry] of Object.entries(state.components)) {
91676
+ const file = manifestSourceFile(key, entry);
91677
+ if (swappedFiles.has(file)) {
91678
+ swappedExports.add(`${file}#${entry.packageExport}`);
91679
+ }
91680
+ }
91681
+ }
91682
+ const alreadySwapped = [];
91550
91683
  const filteredComponents = fullDiscovery.components.filter((c3) => {
91684
+ if (swappedExports.has(`${relPath(c3.filePath)}#${c3.name}`)) {
91685
+ alreadySwapped.push(c3);
91686
+ return false;
91687
+ }
91551
91688
  if (entrySkip.skippedFilePaths.has(c3.filePath)) {
91552
91689
  preBuildIssues.push(
91553
91690
  makeIssue({
@@ -91599,6 +91736,10 @@ async function runImportLocalPipeline(opts) {
91599
91736
  return {
91600
91737
  payload,
91601
91738
  filteredComponents,
91739
+ alreadySwapped,
91740
+ state,
91741
+ target,
91742
+ organization: creds.organization,
91602
91743
  preBuildIssues,
91603
91744
  importPhaseIssues,
91604
91745
  scanSourceFileCount: scan2.sourceFiles.length,
@@ -91636,6 +91777,7 @@ async function runImportSubmit(local, opts, onProgress) {
91636
91777
  const {
91637
91778
  payload,
91638
91779
  filteredComponents,
91780
+ alreadySwapped,
91639
91781
  buildResult,
91640
91782
  baseUrl,
91641
91783
  branchName,
@@ -91678,7 +91820,7 @@ async function runImportSubmit(local, opts, onProgress) {
91678
91820
  });
91679
91821
  }
91680
91822
  const discoveredManifestKeys = new Set(
91681
- filteredComponents.map(
91823
+ [...filteredComponents, ...alreadySwapped].map(
91682
91824
  (component) => manifestKeyForPath(opts.cwd, component.filePath)
91683
91825
  )
91684
91826
  );
@@ -91758,12 +91900,21 @@ async function runImport(opts) {
91758
91900
  try {
91759
91901
  local = await runImportLocalPipeline(opts);
91760
91902
  } catch (err) {
91761
- scanSpinner.fail("Scan failed.");
91903
+ scanSpinner.fail(
91904
+ err instanceof DirectoryLockedError ? "This directory is swapped." : "Scan failed."
91905
+ );
91762
91906
  throw err;
91763
91907
  }
91908
+ const swappedCount = local.alreadySwapped.length;
91764
91909
  scanSpinner.succeed(
91765
- `Found ${bold(pluralize(local.filteredComponents.length, "first-party component"))}.`
91910
+ `Found ${bold(pluralize(local.filteredComponents.length, "first-party component"))}` + (swappedCount > 0 ? ` to import, plus ${pluralize(swappedCount, "swapped component")} already in Zoah` : "") + "."
91911
+ );
91912
+ console.log(` ${dim("This directory:")} ${describeState(local.state)}`);
91913
+ console.log(
91914
+ ` ${dim("This import:")} ${describeTarget(local.target, local.organization)}`
91766
91915
  );
91916
+ const note = organizationNote(local.target, local.organization);
91917
+ if (note) console.log(dim(` ${note}`));
91767
91918
  const tailwind = local.tailwind;
91768
91919
  if (tailwind.available && tailwind.configPath) {
91769
91920
  console.log(` Tailwind config: ${bold(tailwind.configPath)}.`);
@@ -91838,6 +91989,16 @@ async function runImport(opts) {
91838
91989
  console.log(` ${dim("View in Zoah:")} ${bold(url)}`);
91839
91990
  }
91840
91991
  };
91992
+ if (payload.components.length === 0 && swappedCount > 0) {
91993
+ await flushIssueOutput();
91994
+ console.log("");
91995
+ console.log(
91996
+ ` Nothing new to import. The components here are already swapped to ${bold(
91997
+ local.state.kind === "swapped" ? local.state.manifest.name : "its package"
91998
+ )}. Add a component, then run \`zoah import\` again.`
91999
+ );
92000
+ return;
92001
+ }
91841
92002
  if (opts.dryRun) {
91842
92003
  if (opts.swap) {
91843
92004
  console.log(
@@ -91966,6 +92127,7 @@ var init_import = __esm({
91966
92127
  init_cli_import();
91967
92128
  init_dist4();
91968
92129
  init_swap();
92130
+ init_directory_state();
91969
92131
  }
91970
92132
  });
91971
92133
 
@@ -112586,7 +112748,7 @@ import {
112586
112748
  } from "@modelcontextprotocol/sdk/server/mcp.js";
112587
112749
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
112588
112750
  import { z as z59 } from "zod";
112589
- import { existsSync as existsSync10 } from "fs";
112751
+ import { existsSync as existsSync11 } from "fs";
112590
112752
  import { statSync as statSync3 } from "fs";
112591
112753
  function parseJsonObject(text) {
112592
112754
  const value = JSON.parse(text);
@@ -112678,7 +112840,7 @@ async function runMcpServer(options) {
112678
112840
  return `The credential now points at ${credentialInstance}, but this session is open against ${session.baseUrl}. Refusing to send this call to a different instance than the one you are working on: the token would be presented to the wrong server, and on a development machine the two are commonly local dev and production. Nothing was sent. Either restore the credential for ${session.baseUrl} (\`zoah login\` in that directory), or call \`disconnect\` and then \`connect\` to move this session to ${credentialInstance} deliberately.`;
112679
112841
  }
112680
112842
  function loginScope() {
112681
- return existsSync10(zoahPaths.localCredentials(process.cwd())) || existsSync10(legacyOpacityPaths.localCredentials(process.cwd())) ? "local" : "global";
112843
+ return existsSync11(zoahPaths.localCredentials(process.cwd())) || existsSync11(legacyOpacityPaths.localCredentials(process.cwd())) ? "local" : "global";
112682
112844
  }
112683
112845
  let pendingSignIn = null;
112684
112846
  let selectedOrganization = null;
@@ -112758,7 +112920,7 @@ async function runMcpServer(options) {
112758
112920
  pinnedBy: "the human's selection"
112759
112921
  } : null;
112760
112922
  }
112761
- function organizationLabel(fallbackId) {
112923
+ function organizationLabel2(fallbackId) {
112762
112924
  const working = describeWorkingIn(fallbackId);
112763
112925
  return working?.slug ?? working?.id ?? fallbackId;
112764
112926
  }
@@ -112781,7 +112943,7 @@ async function runMcpServer(options) {
112781
112943
  const dropped = session ? {
112782
112944
  project: session.projectId,
112783
112945
  branch: session.branchSlug,
112784
- fromOrganization: organizationLabel(session.orgId),
112946
+ fromOrganization: organizationLabel2(session.orgId),
112785
112947
  toOrganization: to.slug ?? to.organizationId
112786
112948
  } : void 0;
112787
112949
  if (session) await teardownSession();
@@ -114274,7 +114436,7 @@ __export(init_exports, {
114274
114436
  runMcpInit: () => runMcpInit
114275
114437
  });
114276
114438
  import { execFile } from "child_process";
114277
- import { readFile as readFile6, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
114439
+ import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
114278
114440
  import { createInterface } from "readline/promises";
114279
114441
  import path3 from "path";
114280
114442
  function buildServerEntry(opts) {
@@ -114379,7 +114541,7 @@ async function writeHarnessConfig(cwd, target, entry) {
114379
114541
  const filePath = path3.join(cwd, target.file);
114380
114542
  let root = {};
114381
114543
  try {
114382
- const existing = await readFile6(filePath, "utf8");
114544
+ const existing = await readFile7(filePath, "utf8");
114383
114545
  const parsed = JSON.parse(existing);
114384
114546
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
114385
114547
  return null;
@@ -114646,6 +114808,61 @@ async function runPreflight(opts) {
114646
114808
  init_swap();
114647
114809
  init_whoami();
114648
114810
 
114811
+ // src/commands/status.ts
114812
+ init_session();
114813
+ init_directory_state();
114814
+ init_colors();
114815
+ init_import();
114816
+ async function runStatus(opts) {
114817
+ const found = await findCredentials(opts.cwd);
114818
+ const organization = found?.creds.organization;
114819
+ if (found) {
114820
+ console.log(
114821
+ `Signed in as ${bold(found.creds.userEmail)} (${found.scope}) on ${found.creds.baseUrl}.`
114822
+ );
114823
+ console.log(
114824
+ ` ${dim("Organization:")} ${organization ? `${organization.name}${organization.slug ? ` (${organization.slug})` : ""}` : "none chosen, following the web app (run `zoah organization switch`)"}`
114825
+ );
114826
+ } else {
114827
+ console.log("Not signed in. Run `zoah login` first.");
114828
+ }
114829
+ const state = await readDirectoryState(opts.cwd);
114830
+ console.log("");
114831
+ console.log(`${dim("This directory:")} ${describeState(state)}`);
114832
+ if (state.kind !== "new") {
114833
+ const url = buildDesignUrl(
114834
+ (found?.creds.baseUrl ?? opts.baseUrl).replace(/\/$/, ""),
114835
+ state.config
114836
+ );
114837
+ if (url) console.log(` ${dim("Project:")} ${url}`);
114838
+ if (state.manifest) {
114839
+ console.log(
114840
+ ` ${dim("Package:")} ${state.manifest.name}@${state.manifest.version}`
114841
+ );
114842
+ }
114843
+ }
114844
+ const target = resolveImportTarget(state, organization);
114845
+ console.log(
114846
+ `${dim("Next import:")} ${describeTarget(target, organization)}`
114847
+ );
114848
+ const note = organizationNote(target, organization);
114849
+ if (note) console.log(dim(` ${note}`));
114850
+ console.log("");
114851
+ if (state.kind === "imported") {
114852
+ console.log(
114853
+ dim(
114854
+ "Nothing is swapped yet, so you can still move this directory: `zoah organization switch` to import into another organization, or `zoah import --new-project` for a new project here."
114855
+ )
114856
+ );
114857
+ } else if (state.kind === "swapped") {
114858
+ console.log(
114859
+ dim(
114860
+ "Swapped code renders this project's package, so the directory stays on it. `zoah import` adds new components; `zoah import --swap` also swaps them in."
114861
+ )
114862
+ );
114863
+ }
114864
+ }
114865
+
114649
114866
  // src/app.tsx
114650
114867
  init_flow();
114651
114868
  init_errors();
@@ -114653,14 +114870,15 @@ init_components();
114653
114870
  init_session();
114654
114871
  init_client();
114655
114872
  init_organization();
114873
+ init_directory_state();
114656
114874
  init_local();
114657
114875
  init_base_url();
114658
114876
  init_issues_detail();
114659
114877
  init_dist4();
114660
114878
  init_screens();
114661
114879
  init_components();
114662
- import { Box as Box4, Text as Text5 } from "ink";
114663
- import { readFile as readFile5 } from "fs/promises";
114880
+ import { Text as Text5 } from "ink";
114881
+ import { readFile as readFile6 } from "fs/promises";
114664
114882
  import path2 from "path";
114665
114883
 
114666
114884
  // src/ui/steps.tsx
@@ -114712,7 +114930,8 @@ var scan = async (carry) => {
114712
114930
  branch: carry.branch,
114713
114931
  debug: carry.debug
114714
114932
  });
114715
- if (pipeline.payload.components.length === 0) {
114933
+ const upToDate = pipeline.payload.components.length === 0 && pipeline.alreadySwapped.length > 0 && pipeline.state.kind === "swapped" ? pipeline.state.manifest.name : void 0;
114934
+ if (pipeline.payload.components.length === 0 && !upToDate) {
114716
114935
  throw new Error(
114717
114936
  `No components were extractable from this project. Make sure your component files exist under \`${carry.scopePath}\` and use a supported styling approach.`
114718
114937
  );
@@ -114722,7 +114941,7 @@ var scan = async (carry) => {
114722
114941
  const { writeLastPayload: writeLastPayload2 } = await Promise.resolve().then(() => (init_local(), local_exports));
114723
114942
  await writeLastPayload2(process.cwd(), pipeline.payload);
114724
114943
  }
114725
- return payloadToScanReport(pipeline);
114944
+ return { ...payloadToScanReport(pipeline), upToDate };
114726
114945
  };
114727
114946
  var scanLoader = (carry) => loader(
114728
114947
  () => scan(carry),
@@ -114802,7 +115021,7 @@ async function flushIssueSnapshot() {
114802
115021
  }
114803
115022
 
114804
115023
  // src/app.tsx
114805
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
115024
+ import { jsx as jsx4 } from "react/jsx-runtime";
114806
115025
  var titleCase = (s5) => s5.replace(/[-_]+/g, " ").replace(/\b\w/g, (c3) => c3.toUpperCase()).trim();
114807
115026
  function formatRelativeAge(isoTimestamp) {
114808
115027
  const diffMs = Date.now() - new Date(isoTimestamp).getTime();
@@ -114823,7 +115042,7 @@ function formatRelativeAge(isoTimestamp) {
114823
115042
  }
114824
115043
  async function detectProjectName(cwd) {
114825
115044
  try {
114826
- const raw = await readFile5(path2.join(cwd, "package.json"), "utf8");
115045
+ const raw = await readFile6(path2.join(cwd, "package.json"), "utf8");
114827
115046
  const pkg = JSON.parse(raw);
114828
115047
  if (typeof pkg.name === "string" && pkg.name) {
114829
115048
  const bare = pkg.name.startsWith("@") ? pkg.name.split("/")[1] ?? pkg.name : pkg.name;
@@ -114833,15 +115052,6 @@ async function detectProjectName(cwd) {
114833
115052
  }
114834
115053
  return titleCase(path2.basename(cwd));
114835
115054
  }
114836
- async function packageInDeps(cwd, name) {
114837
- try {
114838
- const raw = await readFile5(path2.join(cwd, "package.json"), "utf8");
114839
- const pkg = JSON.parse(raw);
114840
- return Boolean(pkg.dependencies?.[name] ?? pkg.devDependencies?.[name]);
114841
- } catch {
114842
- return false;
114843
- }
114844
- }
114845
115055
  var projectUrl = (baseUrl, config) => {
114846
115056
  if (!config) return void 0;
114847
115057
  if (!config.orgSlug || !config.projectSlug) return void 0;
@@ -114889,27 +115099,9 @@ async function runApp(opts = {}) {
114889
115099
  version: manifest.version,
114890
115100
  npmUrl: `https://npmjs.com/package/${manifest.name}`
114891
115101
  } : void 0;
114892
- const alreadySetUp = manifest !== null && existingSync !== void 0 && await packageInDeps(process.cwd(), manifest.name);
115102
+ const state = await readDirectoryState(process.cwd());
115103
+ const plannedTarget = resolveImportTarget(state, creds?.organization);
114893
115104
  const ui = createFlow();
114894
- if (alreadySetUp && existingSync) {
114895
- ui.log(
114896
- /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, marginY: 1, children: [
114897
- /* @__PURE__ */ jsx4(Text5, { children: "Your project is already connected to Zoah." }),
114898
- /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", marginLeft: 2, children: [
114899
- /* @__PURE__ */ jsxs4(Text5, { children: [
114900
- /* @__PURE__ */ jsx4(Dim, { children: "Zoah " }),
114901
- /* @__PURE__ */ jsx4(Text5, { color: "cyan", underline: true, children: existingSync.zoahUrl })
114902
- ] }),
114903
- /* @__PURE__ */ jsxs4(Text5, { children: [
114904
- /* @__PURE__ */ jsx4(Dim, { children: "Package " }),
114905
- `${existingSync.packageName}@${existingSync.version}`
114906
- ] })
114907
- ] })
114908
- ] })
114909
- );
114910
- await ui.done();
114911
- return;
114912
- }
114913
115105
  const carry = {
114914
115106
  baseUrl,
114915
115107
  scopePath,
@@ -114922,16 +115114,22 @@ async function runApp(opts = {}) {
114922
115114
  userEmail: creds?.userEmail,
114923
115115
  zoahUrl,
114924
115116
  lastScanAge,
114925
- sync: existingSync
115117
+ sync: existingSync,
115118
+ directory: describeState(state),
115119
+ next: describeTarget(plannedTarget, creds?.organization)
114926
115120
  })
114927
115121
  )) {
114928
115122
  ui.log(/* @__PURE__ */ jsx4(Dim, { children: "Run `zoah` when you are ready." }));
114929
115123
  return;
114930
115124
  }
114931
- const projectName = opts.projectName ?? await ui.run(askProjectName(detectedName));
115125
+ const projectName = opts.projectName ?? (plannedTarget.kind === "new" ? await ui.run(askProjectName(detectedName)) : void 0);
114932
115126
  if (!creds) await ui.run(signIn(baseUrl));
114933
- if (!config) await ensureOrganization(ui, baseUrl);
114934
- await ui.run(scanLoader(carry));
115127
+ if (state.kind !== "swapped") await ensureOrganization(ui, baseUrl);
115128
+ const scanReport = await ui.run(scanLoader(carry));
115129
+ if (scanReport.upToDate) {
115130
+ await flushIssueSnapshot();
115131
+ return;
115132
+ }
114935
115133
  let syncResult;
114936
115134
  try {
114937
115135
  syncResult = await ui.run(syncLoader(projectName, carry));
@@ -115128,6 +115326,18 @@ async function main() {
115128
115326
  fail(err);
115129
115327
  }
115130
115328
  });
115329
+ program.command("status").description(
115330
+ "Show this directory's Zoah project, whether it is swapped, and what `zoah import` will do."
115331
+ ).action(async () => {
115332
+ try {
115333
+ await runStatus({
115334
+ cwd: process.cwd(),
115335
+ baseUrl: program.opts().baseUrl ?? DEFAULT_BASE_URL
115336
+ });
115337
+ } catch (err) {
115338
+ fail(err);
115339
+ }
115340
+ });
115131
115341
  const mcp = program.command("mcp").description(
115132
115342
  "Run the Zoah MCP server over stdio for coding agents. Run `zoah mcp init` to set up your harness; reuses `zoah login` credentials."
115133
115343
  ).option(
@@ -115194,6 +115404,10 @@ async function main() {
115194
115404
  "--dry-run",
115195
115405
  `Run the full local pipeline without calling the API or writing project state. Issues snapshot is still written to ${zoahPathDisplay.issues}. Equivalent to \`zoah preflight\`.`,
115196
115406
  false
115407
+ ).option(
115408
+ "--new-project",
115409
+ "Create a new project in the CLI's organization instead of updating the linked one. Not possible once the directory is swapped.",
115410
+ false
115197
115411
  ).action(
115198
115412
  async (path4, options) => {
115199
115413
  try {
@@ -115203,6 +115417,7 @@ async function main() {
115203
115417
  scopePath: path4 ?? ".",
115204
115418
  cwd: process.cwd(),
115205
115419
  swap: options.swap,
115420
+ newProject: options.newProject,
115206
115421
  projectName: g2.projectName,
115207
115422
  branch: g2.branch,
115208
115423
  debug: g2.debug ?? false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoahhq/cli",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Zoah CLI: import React components, publish via Zoah, swap local imports",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Zoah Inc.",