@fourier-labs/harbour 0.1.40 → 0.1.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,7 +35,7 @@ harbour check --app-root <path> [--integrations] [--json] declaration, types, b
35
35
  harbour integrations catalog --app-root <path> [--json] the company's connections as .harbour/integrations.json names them, with approved channels/views per environment
36
36
  harbour integrations request <connection> --reason <text> --app-root <path> [--environment <env>]
37
37
  harbour integrations status --app-root <path> [--json]
38
- harbour connect <company-start-url> once per company; then harbour login | logout
38
+ harbour connect <work-email-or-start-url> once per company; then harbour login | logout
39
39
  harbour productionise --app-root <path> [--wait] [--json] save + preview deployment, prints the protected link
40
40
  harbour status | retry | promote | setup | profile | audience | secrets … --operation <reference>
41
41
  ```
@@ -47,3 +47,5 @@ harbour status | retry | promote | setup | profile | audience | secrets … --op
47
47
  ## Development
48
48
 
49
49
  Built from the [governance control plane](https://github.com/Fourier-Labs-AI/harbour-governance-control-plane) repository: `npm ci`, `npm run build --prefix packages/harbour-cli`, tests in `tests/cli-kit.test.ts` and `tests/cli-packaging-contract.test.ts`. Releases are tagged `cli-v<version>` (a `-rc.N` candidate first, then the stable tag).
50
+
51
+ Connect with `harbour connect name@getlokal.com`: the CLI tries `https://platform.isomorph.ai/start/getlokal.json` and validates the profile before saving it. This uses the first domain label as a guess, not a domain registry; subdomains or companies whose tenant name differs should use their company setup link. Only the guessed tenant name is sent, not the email. Company sign-in and access checks still apply.
@@ -188,7 +188,7 @@ The person you are working with may not be a developer. They say what they want
188
188
 
189
189
  1. CLI, before \`harbour init\` or anything else: compare the installed CLI with the current release and upgrade it whenever it is behind — a CLI that merely runs is not good enough, only a current one is. \`harbour agent-setup\` prints both versions and says plainly when the installed one is missing or older (its \`--json\` result carries \`cli.upgradeRequired\` and \`cli.remediation\`); without that output, compare \`harbour --version\` with \`npx -y @fourier-labs/harbour --version\` yourself. If either says the installed one is missing or older, run \`npm i -g @fourier-labs/harbour\` and confirm \`harbour --version\` now matches, then continue. Every later command runs the *installed* CLI, so a stale one builds an app that passes every local check and is then refused by the deployment pipeline (\`kit_bundle_incompatible\`) minutes later, with nothing in the app to fix. The kit needs Node 22+ on macOS Apple silicon, Linux x64 or Windows x64; it installs and runs its local database, files and gateway without Docker.
190
190
  2. Folder: if the current folder has no \`.harbour/\` directory, run \`harbour init --app-root .\` — an empty folder gets a small starter app, an existing Vite + React app gets the kit files added and nothing overwritten. Then read the "Harbour development kit" block in CLAUDE.md / AGENTS.md; it holds the per-app rules.
191
- 3. Sign-in, needed only for company systems and shipping: run \`harbour login\`. It opens the browser and the person finishes the sign-in there — the one step they do themselves; tell them so in one line. If login says the company is not connected yet, ask them for the Harbour link their IT/admin gave them and run \`harbour connect <link>\` first.
191
+ 3. Sign-in, needed only for company systems and shipping: run \`harbour login\`. It opens the browser and the person finishes the sign-in there — the one step they do themselves; tell them so in one line. If login says the company is not connected yet, ask for their work email and run \`harbour connect <work-email>\` first. This guesses the tenant from the first email-domain label and validates its company profile; it does not grant access. If the lookup fails, ask for the company setup link their IT/admin gave them and run \`harbour connect <link>\`.
192
192
 
193
193
  ## What they say → what you do
194
194
 
@@ -59,7 +59,7 @@ for (let index = 0; index < args.length; index += 1) {
59
59
  const explicitToken = process.env.HARBOUR_TOKEN?.trim() ?? "";
60
60
  const usage = [
61
61
  "Usage:",
62
- " harbour connect <company-start-url>",
62
+ " harbour connect <work-email-or-start-url>",
63
63
  " harbour login | logout",
64
64
  " harbour productionise --app-root <path> [--include <relative-path>]... [--no-wait] [--max-wait <seconds>] [--json]",
65
65
  " harbour status --operation <reference> [--wait] [--max-wait <seconds>] [--json]",
@@ -80,7 +80,7 @@ const usage = [
80
80
  " harbour integrations request <connection> --reason <text> --app-root <path> [--environment <env>] [--operations a,b] [--expires-at <UTC>] [--json]",
81
81
  " harbour integrations status --app-root <path> [--json]",
82
82
  " harbour integrations catalog --app-root <path> [--json] the company's connections as .harbour/integrations.json names them: identifiers, allowed operations, approved channels/views/mailboxes per environment (no app needed)",
83
- "Run `harbour connect <company-start-url>` once, then sign in when Harbour asks.",
83
+ "Run `harbour connect <work-email-or-start-url>` once, then sign in when Harbour asks.",
84
84
  "productionise saves the app, follows its deployment, and prints the protected preview link; promote sends a tested preview to production.",
85
85
  `--max-wait bounds how long productionise, status --wait, retry and promote follow the deployment (default 30 minutes). When it passes the command exits 0 with status RUNNING, the last known deployment state, and the \`${continueCommand("<reference>")}\` that continues the same deployment: a bounded wait is not a failure and never means start another one.`,
86
86
  "For kit apps, productionise first checks that every connection in .harbour/integrations.json has a preview grant and exits 2 (INTEGRATIONS_NOT_READY) with the requests to make, asks governance whether the company's AI setup is ready when the app calls governed AI (exit 2, AI_NOT_READY, with the IT step), then runs the pipeline's kit gate in the local session and refuses (KIT_GATE_FAILED) what CodeBuild would refuse. promote asks the same AI question for production when --app-root names the app.",
@@ -179,7 +179,7 @@ else {
179
179
  let governance;
180
180
  if (testIntegrations) {
181
181
  if (!config)
182
- throw new CliError("CONFIG_REQUIRED", "`--integrations` needs a company connection: run `harbour connect <company-start-url>`.");
182
+ throw new CliError("CONFIG_REQUIRED", "`--integrations` needs a company connection: run `harbour connect <work-email-or-start-url>`.");
183
183
  const token = await companyToken();
184
184
  if (!token)
185
185
  throw new CliError("AUTH_REQUIRED", "`--integrations` needs a Harbour sign-in: run `harbour login`.");
@@ -211,7 +211,7 @@ else {
211
211
  else {
212
212
  const config = resolveConfig(process.env, await loadConfig());
213
213
  if (!config)
214
- throw new CliError("CONFIG_REQUIRED", "Connect Harbour first with `harbour connect <company-start-url>`.");
214
+ throw new CliError("CONFIG_REQUIRED", "Connect Harbour first with `harbour connect <work-email-or-start-url>`.");
215
215
  const url = config.mcpUrl;
216
216
  const tenant = config.tenantId;
217
217
  if (command === "login") {
@@ -24,25 +24,44 @@ export async function saveConfig(config, path = configPath()) {
24
24
  await chmod(path, 0o600);
25
25
  }
26
26
  export async function connect(startUrl, path = configPath()) {
27
- const url = new URL(startUrl);
27
+ const input = startUrl.trim();
28
+ const email = /^[^\s@]+@([a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)+)$/i.exec(input);
29
+ const guessedTenant = email?.[1]?.split(".")[0]?.toLowerCase();
30
+ let url;
31
+ try {
32
+ url = new URL(guessedTenant ? `https://platform.isomorph.ai/start/${guessedTenant}.json` : input);
33
+ }
34
+ catch {
35
+ throw new Error("Enter your work email or the company setup link from your Isomorph start page: `harbour connect <work-email-or-start-url>`.");
36
+ }
28
37
  if (!/^https?:$/.test(url.protocol))
29
38
  throw new Error("Harbour connection URL must be an HTTPS URL.");
30
39
  if (!url.pathname.endsWith(".json")) {
31
40
  url.pathname = `${url.pathname.replace(/\/$/, "")}.json`;
32
41
  }
33
- const response = await fetch(url, { headers: { accept: "application/json" }, redirect: "error" });
34
- if (!response.ok)
35
- throw new Error("Harbour could not read this company connection profile.");
36
- const body = await response.json();
37
- if (!isRecord(body) || body.schema !== "harbour.tenant-start/1.0")
38
- throw new Error("This is not a Harbour connection profile.");
39
- const config = {
40
- schema: "harbour.cli-config/1.0",
41
- tenantId: String(body.tenantId ?? ""),
42
- mcpUrl: String(body.mcpUrl ?? ""),
43
- ...(typeof body.uploadUrl === "string" ? { uploadUrl: body.uploadUrl } : {}),
44
- ...(typeof body.apiUrl === "string" ? { apiUrl: body.apiUrl } : {})
45
- };
42
+ let config;
43
+ try {
44
+ const response = await fetch(url, { headers: { accept: "application/json" }, redirect: "error" });
45
+ if (!response.ok)
46
+ throw new Error("Harbour could not read this company connection profile.");
47
+ const body = await response.json();
48
+ if (!isRecord(body) || body.schema !== "harbour.tenant-start/1.0")
49
+ throw new Error("This is not a Harbour connection profile.");
50
+ config = {
51
+ schema: "harbour.cli-config/1.0",
52
+ tenantId: String(body.tenantId ?? ""),
53
+ mcpUrl: String(body.mcpUrl ?? ""),
54
+ ...(typeof body.uploadUrl === "string" ? { uploadUrl: body.uploadUrl } : {}),
55
+ ...(typeof body.apiUrl === "string" ? { apiUrl: body.apiUrl } : {})
56
+ };
57
+ if (!validConfig(config) || (guessedTenant && config.tenantId !== guessedTenant))
58
+ throw new Error("This is not the expected Harbour connection profile.");
59
+ }
60
+ catch (error) {
61
+ if (guessedTenant)
62
+ throw new Error("Harbour could not find your company from that email. Ask for the company setup link from your Isomorph start page, then run `harbour connect <company-start-url>`.");
63
+ throw error;
64
+ }
46
65
  await saveConfig(config, path);
47
66
  return config;
48
67
  }
@@ -232,7 +232,7 @@ export async function integrationsCatalog(client) {
232
232
  connection: entry.connection, kind: entry.kind, ...(entry.provider ? { provider: entry.provider } : {}), ...(entry.connector ? { connector: entry.connector } : {}), ...(entry.installation ? { installation: entry.installation } : {}),
233
233
  displayName: entry.displayName, status: entry.status,
234
234
  operations: entry.operations.map(item => ({ operation: item.operation, identities: [...item.identities] })),
235
- resources: Object.fromEntries(ENVIRONMENTS.map(environment => [environment, (entry.resources[environment] ?? []).map(resource => ({ name: resource.name, ...(resource.columns ? { columns: [...resource.columns] } : {}) }))]))
235
+ resources: Object.fromEntries(ENVIRONMENTS.map(environment => [environment, (entry.resources[environment] ?? []).map(resource => ({ name: resource.name, ...(resource.columns ? { columns: [...resource.columns] } : {}), ...(resource.description ? { description: resource.description } : {}) }))]))
236
236
  })) };
237
237
  }
238
238
  const PROVIDER_WORDS = { slack: "Slack", gmail: "Gmail" };
@@ -248,7 +248,7 @@ export function renderIntegrationsCatalog(result) {
248
248
  lines.push(`\`${entry.connection}\` — ${what} "${entry.displayName}" (${state})`);
249
249
  lines.push(` operations: ${entry.operations.map(item => `${item.operation} (${item.identities.join(" or ")})`).join(", ") || "none allowed"}`);
250
250
  for (const environment of ENVIRONMENTS) {
251
- const names = entry.resources[environment].map(resource => `${resource.name}${resource.columns?.length ? ` (${resource.columns.join(", ")})` : ""}`);
251
+ const names = entry.resources[environment].map(resource => `${resource.name}${resource.description ? ` — ${resource.description}` : ""}${resource.columns?.length ? ` (${resource.columns.join(", ")})` : ""}`);
252
252
  lines.push(` ${environment}: ${names.join(", ") || "nothing approved yet"}`);
253
253
  }
254
254
  }
@@ -1 +1 @@
1
- export const CLI_VERSION = "0.1.40";
1
+ export const CLI_VERSION = "0.1.41";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fourier-labs/harbour",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "description": "Harbour productionisation helper",
5
5
  "type": "module",
6
6
  "bin": {