@fourier-labs/harbour 0.1.45 → 0.1.46
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.
|
@@ -200,7 +200,7 @@ The person you are working with may not be a developer. They say what they want
|
|
|
200
200
|
- "ship it", "put it online", "let my team try it" → run \`harbour check --app-root . --json\` first and fix everything it finds, every time, unasked: the same gates run again in the cloud, where each failed attempt costs minutes instead of the seconds it costs here. Before sharing or deploying the private preview, show the person the exact proposed app name, description and audience that will be passed to Harbour (say "only you" when the audience is empty) and ask for one confirmation or correction covering all three; do not run \`productionise\` until they confirm. Pass that confirmed setup in the same command: \`harbour productionise --app-root . --name "<app name>" --description "<description>" --emails "<comma-separated audience>" --wait --json\`; omit \`--emails\` for "only you". The command records the confirmed setup before uploading or deploying the app. Then each declared connection needs a preview grant (\`harbour integrations request <connection> --environment preview --reason "…" --app-root . --json\`); when the app calls governed AI, \`productionise\` also asks whether the company's AI setup is ready and refuses with \`AI_NOT_READY\` and the one IT step — say that line and nothing more; the app works without AI until then. The command gives them \`result.deployment.protectedUrl\`: a private preview that they, and the people they confirmed, open after company sign-in. If your tool cuts the command off before it finishes, \`${continueCommand("<ref>")}\` continues the same deployment — never start another one to find out what happened. For kit apps, describe \`TRANSFORMING\` as building and checking the app; it does not mean a transformation AI is running. The CLI saves \`operationRef\` in \`.harbour/local/productionise.json\`; repeating \`productionise\` continues that operation. Keep \`operationRef\`; \`harbour setup --operation <ref> --json\` lists what is still missing (name, audience, secrets) and \`harbour profile\` / \`harbour audience\` / \`harbour secrets set\` fill it in.
|
|
201
201
|
- "make it live for everyone", "go to production" → only after they have tried the preview. Before \`harbour promote --operation <ref> --json\`, show the exact app name, description and production audience again and ask for one confirmation or correction covering all three; never promote a profile or audience the person has not just seen and confirmed. Then promote with the operation reference from productionise. Report the production link, or that an operator approval is pending.
|
|
202
202
|
- "stop it" → \`harbour stop --app-root .\` (local data kept). \`harbour dev --reset --app-root .\` deletes local data — only when they explicitly ask to start over.
|
|
203
|
-
- "every day at 2pm", "run this on a schedule", "send this automatically" → create one \`jobs/<name>.ts\` file with a literal UTC cron
|
|
203
|
+
- "every day at 2pm", "run this on a schedule", "send this automatically" → create one \`jobs/<name>.ts\` file with a literal UTC cron and one default async handler. Start \`harbour dev\`, then run it immediately with \`harbour jobs run <name> --app-root . --scheduled-at <matching-UTC-time> --json\`; this checks the job against local data and safe fixtures. If the person explicitly asks to test the company action now, use \`--real\` only after the exact action has development approval. Tell them: "Your scheduled task is ready. I can test the app on your computer now. Company messages will start after the app is online and access is approved." Never use \`setInterval\`, an effect or a browser timer as a scheduler; Harbour runs the same declaration automatically after deployment.
|
|
204
204
|
|
|
205
205
|
When a command refuses, the refusal names its own reason and its own fix: change that one thing, then run it again. A failed app needs its reported fix; a wait timeout means work is still running, so continue the saved operation — and never start a second deploy of an app while one is running, because concurrent deploys of one app cancel each other.
|
|
206
206
|
|
|
@@ -38,6 +38,7 @@ const environment = optionValue("--environment");
|
|
|
38
38
|
const operations = optionValue("--operations");
|
|
39
39
|
const expiresAt = optionValue("--expires-at");
|
|
40
40
|
const scheduledAt = optionValue("--scheduled-at");
|
|
41
|
+
const realJob = args.includes("--real");
|
|
41
42
|
/** Seconds `productionise`, `status --wait`, `retry` and `promote` follow the operation before reporting it as still running (default: 30 minutes). */
|
|
42
43
|
const maxWaitSeconds = args.includes("--max-wait") ? Number(optionValue("--max-wait")) : undefined;
|
|
43
44
|
const waitOptions = maxWaitSeconds ? { maxWaitMs: maxWaitSeconds * 1000 } : {};
|
|
@@ -78,7 +79,7 @@ const usage = [
|
|
|
78
79
|
" harbour dev --app-root <path> [--reset] run the app locally on one loopback origin (--reset deletes this app's local data)",
|
|
79
80
|
" harbour stop --app-root <path> stop this app's local services, keeping its database and files",
|
|
80
81
|
" harbour check --app-root <path> [--integrations] [--json] types, build, then the pipeline's kit gate in the local session: declaration, migrations + database gate, write probe with cross-user denial, journeys, operation coverage (+ authorised real reads)",
|
|
81
|
-
" harbour jobs run <name> --app-root <path> [--scheduled-at <UTC>] [--json] run one scheduled job now
|
|
82
|
+
" harbour jobs run <name> --app-root <path> [--scheduled-at <UTC>] [--real] [--json] run one scheduled job now (local fixtures; --real uses approved company access)",
|
|
82
83
|
" harbour integrations request <connection> --reason <text> --app-root <path> [--environment <env>] [--operations a,b] [--expires-at <UTC>] [--json]",
|
|
83
84
|
" harbour integrations status --app-root <path> [--json]",
|
|
84
85
|
" harbour integrations catalog --app-root <path> [--json] the company's connections as .harbour/integrations.json names them: identifiers, allowed operations, approved channels/tables/views/mailboxes per environment (no app needed)",
|
|
@@ -196,7 +197,7 @@ else {
|
|
|
196
197
|
emit(summaryEnvelope(report));
|
|
197
198
|
}
|
|
198
199
|
else if (command === "jobs") {
|
|
199
|
-
const result = await runJob(target, args[2], scheduledAt, runCommand);
|
|
200
|
+
const result = await runJob(target, args[2], scheduledAt, runCommand, realJob);
|
|
200
201
|
progress(`Job ${result.name} completed for ${result.scheduledAt}.`);
|
|
201
202
|
emit(summaryEnvelope(result));
|
|
202
203
|
}
|
|
@@ -27,7 +27,7 @@ export async function discoverJobs(root) {
|
|
|
27
27
|
}
|
|
28
28
|
return jobs.sort((a, b) => a.name.localeCompare(b.name));
|
|
29
29
|
}
|
|
30
|
-
export async function runJob(root, name, scheduledAt, run = runCommand) {
|
|
30
|
+
export async function runJob(root, name, scheduledAt, run = runCommand, real = false) {
|
|
31
31
|
if (!jobName.test(name))
|
|
32
32
|
throw new CliError("JOB_INVALID", "A valid job name is required.");
|
|
33
33
|
const job = (await discoverJobs(root)).find(candidate => candidate.name === name);
|
|
@@ -42,8 +42,12 @@ export async function runJob(root, name, scheduledAt, run = runCommand) {
|
|
|
42
42
|
throw new CliError("DEV_NOT_RUNNING", "Start `harbour dev` before running a job so it uses the same local data and identity boundary.");
|
|
43
43
|
const env = parseSessionEnv(session);
|
|
44
44
|
const runner = `const module = await import(${JSON.stringify(pathToFileURL(job.path).href)}); if (typeof module.default !== "function") throw new Error("job has no default handler"); await module.default({scheduledAt: process.env.HARBOUR_SCHEDULED_AT});`;
|
|
45
|
-
|
|
45
|
+
// Ordinary runs stay fixture-only. An explicit real run uses the same
|
|
46
|
+
// loopback origin as the browser, where the existing forwarder applies the
|
|
47
|
+
// builder's approved company/AI access.
|
|
48
|
+
const gatewayUrl = real ? lock.origin : `http://127.0.0.1:${lock.ports.gateway}`;
|
|
49
|
+
const result = await run(process.execPath, ["--experimental-strip-types", "--input-type=module", "--eval", runner], { cwd: root, env: { ...env, HARBOUR_GATEWAY_URL: gatewayUrl, HARBOUR_SCHEDULED_AT: timestamp } });
|
|
46
50
|
if (result.code !== 0)
|
|
47
51
|
throw new CliError("JOB_FAILED", `Job ${name} failed: ${result.stderr.trim().split("\n").at(-1) ?? "process exited non-zero"}`);
|
|
48
|
-
return { name, schedule: job.schedule, scheduledAt: timestamp };
|
|
52
|
+
return { name, schedule: job.schedule, scheduledAt: timestamp, mode: real ? "real" : "local" };
|
|
49
53
|
}
|
|
@@ -93,7 +93,7 @@ export function managedBlock() {
|
|
|
93
93
|
"- Identity, data and files go through `@harbour/app-sdk` only: `harbour.identity.current()`, `harbour.data.from(table)`, `harbour.files.*`. Never open a database, storage bucket or company system from browser code, and never `fetch` a company URL directly.",
|
|
94
94
|
"- Company systems (Slack, Gmail, warehouse views) are reached only through `harbour.integrations.execute(connection, {operation, resource, input})` with the connection and operation declared in `.harbour/integrations.json`. Operations are a closed set: `slack.channel.history` (user identity), `slack.message.post` (declared per app: `app` posts as the company's Slack bot, Isomorph AI, under the name IT approved; `user` posts as the signed-in person after their consent), `gmail.thread.list`, `gmail.message.read` and `gmail.message.send` (user identity — reads and one plain-text send as the signed-in person, resource `inbox`), `warehouse.view.read` (app identity). Resources are logical names, never IDs, URLs or tokens.",
|
|
95
95
|
"- `.harbour/integrations.json` starts with `\"connections\": {}` and stays that way until the app really calls a company system. Adding one is two steps: declare the connection — by the identifier `harbour integrations catalog --app-root .` lists, with only the operations the app calls and only channels, views and mailboxes the catalog shows as approved; never a guessed name — then `harbour integrations request <connection> --reason \"<why>\" --app-root .` (and the same command with `--environment preview` before shipping). Every declared connection blocks the deploy until IT grants it, so a connection the app does not call is a deploy that never happens; a connection that is not declared cannot be requested at all (`CONNECTION_NOT_DECLARED`), so it never gets a grant. Before a real integration test, read `harbour integrations status --app-root . --json`: show pending IT approval separately from personal consent, and test ready destinations independently. After a partial send, retry only the failed destination with its original idempotency key; do not regenerate or resend a successful destination. The file is strict JSON and cannot hold comments; the worked Slack and warehouse examples are in README.md and in the comment in `src/App.tsx`.",
|
|
96
|
-
"- Scheduled work lives only in `jobs/<name>.ts`: export one literal UTC cron as `schedule` and one default async handler. Test it immediately
|
|
96
|
+
"- Scheduled work lives only in `jobs/<name>.ts`: export one literal UTC cron as `schedule` and one default async handler. Test it immediately with `harbour jobs run <name> --app-root . --scheduled-at <UTC> --json`; this uses local data and safe fixtures. If the person explicitly asks for a company-action test, use `--real` only after the exact action has development approval. Tell them: \"Your scheduled task is ready. I can test the app on your computer now. Company messages will start after the app is online and access is approved.\" Harbour runs the same declaration automatically after deployment; never use `setInterval`, an effect or a browser timer as a scheduler.",
|
|
97
97
|
"- In browser code, a Slack message is sent only after the person presses an explicit Send control; pass a fresh UUID `idempotencyKey` per send action and reuse the same key when retrying that action. Never send from an effect or browser timer, and never send during checks. A `jobs/*.ts` handler is the only scheduled-send path and acts as the app: only an app-mode operation may run there, after the person explicitly requested it and IT granted its exact destination for that environment. Use a deterministic idempotency key for the business period and destination; local job runs and checks use fixtures and send nothing.",
|
|
98
98
|
"- A Slack message is posted either as the app (the company's Slack bot, Isomorph AI, under the IT-approved name: declare `\"presentation\": { \"displayName\": \"<app name>\" }` on the Slack connection, optional `iconEmoji`; every app-mode post ends with \"Posted by <app> on Isomorph\") or as the person (`\"identity\": \"user\"` on `slack.message.post`, their own consent; an older consent answers `USER_RECONNECT_REQUIRED` — offer Connect again) — never pretend one is the other. The declaration is the mode you request access for; when the app is approved for both, every post says which with `mode: \"app\"` or `mode: \"user\"` on the call (`MODE_REQUIRED` otherwise), and a mode IT has not approved is refused with `MODE_NOT_GRANTED`, never swapped. A post needs the bot in the channel: `/invite @Isomorph AI` there first.",
|
|
99
99
|
"- Consent is only for user-identity operations (`slack.channel.history`, `gmail.*`, and `slack.message.post` declared `\"identity\": \"user\"`): call `harbour.integrations.connect(connection)` and, when it returns `consent_required`, open `authorizationUrl`. App-identity operations (`slack.message.post` declared `\"identity\": \"app\"`, `warehouse.view.read`) never call connect — it is refused as unapproved user access. Missing consent never falls back to another account.",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "0.1.
|
|
1
|
+
export const CLI_VERSION = "0.1.46";
|