@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 +3 -1
- package/dist/packages/harbour-cli/src/agent-setup.js +1 -1
- package/dist/packages/harbour-cli/src/cli.js +4 -4
- package/dist/packages/harbour-cli/src/config.js +33 -14
- package/dist/packages/harbour-cli/src/integrations.js +2 -2
- package/dist/packages/harbour-cli/src/version.js +1 -1
- package/package.json +1 -1
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 <
|
|
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
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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 <
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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.
|
|
1
|
+
export const CLI_VERSION = "0.1.41";
|