@njeybe/nativ 2.5.1 → 2.7.0
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/CHANGELOG.md +67 -0
- package/README.md +100 -5
- package/dist/commands/design-check.d.ts +4 -0
- package/dist/commands/design-check.js +76 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +55 -11
- package/dist/commands/notify.d.ts +2 -0
- package/dist/commands/notify.js +25 -0
- package/dist/commands/session.d.ts +22 -0
- package/dist/commands/session.js +106 -0
- package/dist/commands/task/task-add.js +1 -1
- package/dist/commands/task/task-common.js +6 -6
- package/dist/commands/task/task-escalate.js +8 -10
- package/dist/commands/task/task-lifecycle.d.ts +7 -1
- package/dist/commands/task/task-lifecycle.js +30 -5
- package/dist/commands/task/task-query.d.ts +2 -0
- package/dist/commands/task/task-query.js +27 -12
- package/dist/commands/task/task-reclaim.d.ts +20 -0
- package/dist/commands/task/task-reclaim.js +87 -0
- package/dist/commands/task.d.ts +2 -0
- package/dist/commands/task.js +1 -0
- package/dist/commands/triage.js +3 -3
- package/dist/commands/update.js +46 -0
- package/dist/commands/validate.js +1 -0
- package/dist/commands/verify.js +21 -5
- package/dist/core/design-rules.d.ts +23 -0
- package/dist/core/design-rules.js +115 -0
- package/dist/core/enforcement.js +18 -17
- package/dist/core/harness-integrity.js +26 -7
- package/dist/core/lock-manager.d.ts +6 -0
- package/dist/core/lock-manager.js +50 -41
- package/dist/core/notify.d.ts +38 -0
- package/dist/core/notify.js +133 -0
- package/dist/core/profiles.js +7 -16
- package/dist/core/session-memory.d.ts +42 -0
- package/dist/core/session-memory.js +121 -0
- package/dist/core/setup/pm-playbook.d.ts +5 -0
- package/dist/core/setup/pm-playbook.js +26 -0
- package/dist/core/setup/session-context.js +35 -2
- package/dist/core/setup/setup-planner.js +2 -2
- package/dist/core/setup/types.d.ts +2 -0
- package/dist/core/setup/types.js +5 -1
- package/dist/core/task-claims.d.ts +15 -0
- package/dist/core/task-claims.js +32 -0
- package/dist/core/tier1-liaison.d.ts +1 -0
- package/dist/core/tier1-liaison.js +25 -27
- package/dist/core/verifier/output-digest.d.ts +19 -0
- package/dist/core/verifier/output-digest.js +48 -0
- package/dist/db/data-engine.js +33 -32
- package/dist/db/introspector.js +61 -60
- package/dist/db/sqlite.d.ts +3 -0
- package/dist/db/sqlite.js +10 -0
- package/dist/governor/circuit-breaker.js +12 -0
- package/dist/governor/patch-applier.d.ts +1 -0
- package/dist/governor/patch-applier.js +3 -7
- package/dist/index.js +40 -6
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +53 -14
- package/dist/providers/claude-api.js +3 -0
- package/dist/providers/gemini.js +4 -1
- package/dist/providers/registry.d.ts +6 -2
- package/dist/providers/registry.js +28 -16
- package/dist/providers/types.d.ts +4 -1
- package/dist/runner/agent-supervisor.js +2 -2
- package/dist/runner/cli-config.js +1 -1
- package/dist/scanner/types.d.ts +5 -1
- package/dist/scanner/types.js +7 -0
- package/dist/server/routes/task-routes.d.ts +2 -0
- package/dist/server/routes/triage-routes.js +3 -18
- package/dist/server/studio-ui/pages/benchmarks/styles.js +8 -5
- package/dist/server/studio-ui/pages/database/browser.js +1 -1
- package/dist/server/studio-ui/pages/database/connection.js +1 -1
- package/dist/server/studio-ui/pages/database/data-shell.js +5 -5
- package/dist/server/studio-ui/pages/database/drift.js +1 -1
- package/dist/server/studio-ui/pages/database/explorer.js +2 -2
- package/dist/server/studio-ui/pages/database/styles.js +5 -5
- package/dist/server/studio-ui/pages/flow/styles.js +3 -3
- package/dist/server/studio-ui/pages/home/script.js +110 -91
- package/dist/server/studio-ui/pages/home/styles.js +33 -45
- package/dist/server/studio-ui/pages/tasks/script.js +150 -3
- package/dist/server/studio-ui/pages/tasks/styles.js +63 -6
- package/dist/server/studio-ui/pages/team/script.js +49 -52
- package/dist/server/studio-ui/pages/team/styles.js +21 -20
- package/dist/server/studio-ui/pages/worktrees/script.js +47 -35
- package/dist/server/studio-ui/pages/worktrees/styles.js +12 -10
- package/dist/server/studio-ui/shell/brand.d.ts +4 -0
- package/dist/server/studio-ui/shell/brand.js +16 -0
- package/dist/server/studio-ui/shell/console.js +61 -2
- package/dist/server/studio-ui/shell/core.js +8 -8
- package/dist/server/studio-ui/shell/detail-view.js +1 -1
- package/dist/server/studio-ui/shell/dispatch.js +18 -0
- package/dist/server/studio-ui/shell/events-tools.js +5 -1
- package/dist/server/studio-ui/shell/format.js +5 -6
- package/dist/server/studio-ui/shell/icons.js +23 -15
- package/dist/server/studio-ui/shell/markup-head.js +11 -6
- package/dist/server/studio-ui/shell/markup-tools.js +19 -10
- package/dist/server/studio-ui/shell/paint.js +3 -0
- package/dist/server/studio-ui/shell/pipeline-state.js +1 -1
- package/dist/server/studio-ui/shell/proposals.js +6 -2
- package/dist/server/studio-ui/shell/pulse.d.ts +1 -0
- package/dist/server/studio-ui/shell/pulse.js +67 -0
- package/dist/server/studio-ui/shell/router.js +25 -0
- package/dist/server/studio-ui/shell/sidebar-view.js +6 -3
- package/dist/server/studio-ui/shell/toasts-format.js +4 -4
- package/dist/server/studio-ui/styles/console.js +33 -13
- package/dist/server/studio-ui/styles/dialogs.js +4 -4
- package/dist/server/studio-ui/styles/responsive.js +8 -0
- package/dist/server/studio-ui/styles/shell.js +24 -5
- package/dist/server/studio-ui/styles/strategist.js +10 -10
- package/dist/server/studio-ui/styles/tokens-base.js +23 -23
- package/dist/server/studio-ui.js +2 -0
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/agents/architect.md +11 -12
- package/plugin/agents/explorer.md +1 -1
- package/plugin/agents/verifier.md +2 -2
- package/plugin/agents/worker.md +5 -7
- package/plugin/skills/nativ/SKILL.md +36 -49
- package/templates/AGENTS.md +36 -49
- package/templates/CLAUDE.md +1 -41
- package/templates/GEMINI.md +3 -2
- package/templates/claude-agents/architect.md +11 -12
- package/templates/claude-agents/explorer.md +1 -1
- package/templates/claude-agents/verifier.md +2 -2
- package/templates/claude-agents/worker.md +5 -7
- package/templates/design/taste/LICENSE +21 -0
- package/templates/design/taste/SOURCE.md +28 -0
- package/templates/design/taste/v1/DIGEST.md +57 -0
- package/templates/design/taste/v1/SKILL.md +226 -0
- package/templates/design/taste/v2/DIGEST.md +62 -0
- package/templates/design/taste/v2/SKILL.md +1206 -0
- package/templates/dot-ai/api_contracts.json +66 -66
- package/templates/dot-ai/context.md +38 -38
- package/templates/dot-ai/db_schema.json +39 -39
- package/templates/dot-ai/subagents/architect.md +6 -0
- package/templates/dot-ai/subagents/flutter-developer.md +2 -11
- package/templates/dot-ai/subagents/frontend.md +47 -44
- package/templates/dot-ai/ui_specs.md +15 -36
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { writeJsonAtomicSync } from '../core/lock-manager.js';
|
|
3
4
|
import { createClaudeApiProvider } from './claude-api.js';
|
|
4
5
|
import { createClaudeCliProvider, DEFAULT_LIMIT_COOLDOWN_MS } from './claude-cli.js';
|
|
5
6
|
import { createGeminiProvider } from './gemini.js';
|
|
@@ -42,11 +43,7 @@ function readCooldowns(root) {
|
|
|
42
43
|
}
|
|
43
44
|
function writeCooldowns(root, cooldownUntil) {
|
|
44
45
|
try {
|
|
45
|
-
|
|
46
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
47
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
48
|
-
fs.writeFileSync(tmp, JSON.stringify({ cooldownUntil }, null, 2), 'utf8');
|
|
49
|
-
fs.renameSync(tmp, file);
|
|
46
|
+
writeJsonAtomicSync(path.join(root, STATE_FILE), { cooldownUntil });
|
|
50
47
|
}
|
|
51
48
|
catch {
|
|
52
49
|
// Cooldown is an optimisation; failing to persist it must never fail the call.
|
|
@@ -93,10 +90,17 @@ export function resolveProvider(role, root, options = {}) {
|
|
|
93
90
|
return resolveProviderChain(role, root, options)[0] ?? null;
|
|
94
91
|
}
|
|
95
92
|
/**
|
|
96
|
-
* Runs the request against each provider in turn. A
|
|
97
|
-
*
|
|
93
|
+
* Runs the request against each provider in turn. A TRANSIENT failure (network, 5xx) is retried on the same
|
|
94
|
+
* provider up to MAX_TRANSIENT_RETRIES times with jittered backoff. A rate limit puts that provider on cooldown, and
|
|
95
|
+
* any other ProviderError falls through to the next one. Throws NO_CREDENTIALS when nothing is available and the
|
|
98
96
|
* last error when every provider failed, so the caller can drop to its deterministic path.
|
|
99
97
|
*/
|
|
98
|
+
export const MAX_TRANSIENT_RETRIES = 2;
|
|
99
|
+
/** 0.5s, then 1s, each plus up to 50% jitter so parallel agents do not retry in lockstep. */
|
|
100
|
+
export function transientBackoffMs(attempt, random = Math.random) {
|
|
101
|
+
const base = 500 * 2 ** attempt;
|
|
102
|
+
return Math.round(base + base * 0.5 * random());
|
|
103
|
+
}
|
|
100
104
|
export async function completeWithChain(role, root, request, options = {}) {
|
|
101
105
|
const chain = resolveProviderChain(role, root, options);
|
|
102
106
|
if (!chain.length)
|
|
@@ -104,17 +108,25 @@ export async function completeWithChain(role, root, request, options = {}) {
|
|
|
104
108
|
const models = (options.config ?? loadProviderConfig(root)).models ?? {};
|
|
105
109
|
const now = options.deps?.now ?? Date.now;
|
|
106
110
|
let lastError = null;
|
|
111
|
+
const sleep = options.deps?.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
107
112
|
for (const provider of chain) {
|
|
108
113
|
const model = request.model ?? models[provider.id] ?? provider.defaultModel;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
for (let attempt = 0;; attempt++) {
|
|
115
|
+
try {
|
|
116
|
+
const text = await provider.complete({ ...request, model }, root);
|
|
117
|
+
return { provider: provider.id, model, text };
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const failure = err instanceof ProviderError ? err : new ProviderError('BAD_RESPONSE', err instanceof Error ? err.message : String(err));
|
|
121
|
+
lastError = failure;
|
|
122
|
+
if (failure.code === 'TRANSIENT' && attempt < MAX_TRANSIENT_RETRIES) {
|
|
123
|
+
await sleep(transientBackoffMs(attempt));
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (failure.code === 'RATE_LIMITED')
|
|
127
|
+
setProviderCooldown(root, provider.id, failure.retryAfterMs ?? DEFAULT_LIMIT_COOLDOWN_MS, now());
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
118
130
|
}
|
|
119
131
|
}
|
|
120
132
|
throw lastError ?? new ProviderError('BAD_RESPONSE', 'Every provider failed.');
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
export type ProviderId = 'claude-cli' | 'claude-api' | 'gemini';
|
|
8
8
|
/** Roles a provider can be assigned to in `.nativ/config.json`. */
|
|
9
9
|
export type ProviderRole = 'architect' | 'pm' | 'worker' | 'verifier' | 'triage';
|
|
10
|
-
|
|
10
|
+
/** TRANSIENT: a network failure or a 5xx answer, worth retrying on the same provider after a short wait. */
|
|
11
|
+
export type ProviderErrorCode = 'NO_CREDENTIALS' | 'TIMEOUT' | 'RATE_LIMITED' | 'TRANSIENT' | 'BAD_RESPONSE';
|
|
11
12
|
/** Typed failure so callers can fall through the provider chain without string matching. */
|
|
12
13
|
export declare class ProviderError extends Error {
|
|
13
14
|
readonly code: ProviderErrorCode;
|
|
@@ -57,6 +58,8 @@ export interface ProviderDeps {
|
|
|
57
58
|
anthropicClient?: () => AnthropicLike;
|
|
58
59
|
env?: NodeJS.ProcessEnv;
|
|
59
60
|
now?: () => number;
|
|
61
|
+
/** Waits between retries; injectable so tests do not sleep. */
|
|
62
|
+
sleep?: (ms: number) => Promise<void>;
|
|
60
63
|
}
|
|
61
64
|
export interface AnthropicLike {
|
|
62
65
|
messages: {
|
|
@@ -5,6 +5,7 @@ import fs from 'node:fs';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import Anthropic from '@anthropic-ai/sdk';
|
|
7
7
|
import { loadMasterPlan } from '../core/verifier.js';
|
|
8
|
+
import { writeJsonAtomicSync } from '../core/lock-manager.js';
|
|
8
9
|
import { cacheHitRate, computeActualCostUsd, emptyActualUsage, recordRunnerUsage, } from '../core/telemetry.js';
|
|
9
10
|
import { resolveProjectRoot, linkWorktreeAiDirectory, safeUnlinkWorktreeAiDirectory, linkWorktreeNodeModules, safeUnlinkWorktreeNodeModules, } from '../core/root-resolver.js';
|
|
10
11
|
import { resolveWorkerModel, toNativeModelId } from '../core/model-routing.js';
|
|
@@ -713,8 +714,7 @@ export class AgentSupervisor extends EventEmitter {
|
|
|
713
714
|
}
|
|
714
715
|
persist(record) {
|
|
715
716
|
try {
|
|
716
|
-
|
|
717
|
-
fs.writeFileSync(path.join(this.runsDir, `${record.taskId}.json`), `${JSON.stringify(record, null, 2)}\n`, 'utf8');
|
|
717
|
+
writeJsonAtomicSync(path.join(this.runsDir, `${record.taskId}.json`), record);
|
|
718
718
|
}
|
|
719
719
|
catch {
|
|
720
720
|
// Local-first persistence is advisory; in-memory state stays authoritative.
|
|
@@ -77,7 +77,7 @@ export function buildRunnerPrompt(task) {
|
|
|
77
77
|
task.description ? `Description: ${task.description}.` : '',
|
|
78
78
|
task.verificationCommand ? `Verify your work using: ${task.verificationCommand}.` : '',
|
|
79
79
|
`Start by running: nativ task start ${task.id}. When finished and verified, run: nativ task complete ${task.id}.`,
|
|
80
|
-
`If blocked, follow
|
|
80
|
+
`If blocked, follow "Talking to the human" in AGENTS.md: explain the user experience symptom, root cause in plain English, and clear options without technical jargon.`,
|
|
81
81
|
]
|
|
82
82
|
.filter(Boolean)
|
|
83
83
|
.join(' ');
|
package/dist/scanner/types.d.ts
CHANGED
|
@@ -42,6 +42,9 @@ export interface MasterPlanTask {
|
|
|
42
42
|
complexity?: TaskComplexity;
|
|
43
43
|
/** Plain-language "done when" lines. */
|
|
44
44
|
acceptanceCriteria?: string[];
|
|
45
|
+
/** Set by `task start` while in_progress: who claimed it (NATIV_AGENT_ID or --agent) and when. */
|
|
46
|
+
claimedBy?: string;
|
|
47
|
+
claimedAt?: string;
|
|
45
48
|
}
|
|
46
49
|
export type TaskComplexity = 'simple' | 'standard' | 'complex';
|
|
47
50
|
export declare const TASK_COMPLEXITIES: readonly TaskComplexity[];
|
|
@@ -64,7 +67,8 @@ export interface MasterPlan {
|
|
|
64
67
|
activeMilestoneId: string;
|
|
65
68
|
milestones: MasterPlanMilestone[];
|
|
66
69
|
}
|
|
67
|
-
export
|
|
70
|
+
export declare const ESCALATION_TYPES: readonly ["contract_drift", "schema_flaw", "missing_credential", "dependency_conflict", "architectural_ambiguity"];
|
|
71
|
+
export type EscalationType = (typeof ESCALATION_TYPES)[number];
|
|
68
72
|
export interface EscalationRecord {
|
|
69
73
|
id: string;
|
|
70
74
|
taskId: string;
|
package/dist/scanner/types.js
CHANGED
|
@@ -71,6 +71,8 @@ export declare function handlePipelineTasks(root: string): {
|
|
|
71
71
|
specRefs?: string[];
|
|
72
72
|
complexity?: import("../../scanner/types.js").TaskComplexity;
|
|
73
73
|
acceptanceCriteria?: string[];
|
|
74
|
+
claimedBy?: string;
|
|
75
|
+
claimedAt?: string;
|
|
74
76
|
}[];
|
|
75
77
|
id: string;
|
|
76
78
|
name: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { withFileLock, withPlanLock } from '../../core/lock-manager.js';
|
|
3
|
+
import { withFileLock, withPlanLock, writeJsonAtomicSync } from '../../core/lock-manager.js';
|
|
4
4
|
import { CircuitBreaker, applySelfHealingProposal, } from '../../governor/index.js';
|
|
5
5
|
import { finalizeTriage, triageBlocker } from '../../commands/triage.js';
|
|
6
6
|
import { HttpError } from '../http-utils.js';
|
|
@@ -19,7 +19,7 @@ export function readEscalationFile(root) {
|
|
|
19
19
|
catch {
|
|
20
20
|
return null;
|
|
21
21
|
}
|
|
22
|
-
//
|
|
22
|
+
// Older versions left an empty file behind when they first took its lock.
|
|
23
23
|
if (!raw.trim())
|
|
24
24
|
return null;
|
|
25
25
|
try {
|
|
@@ -34,22 +34,7 @@ export function readEscalationFile(root) {
|
|
|
34
34
|
}
|
|
35
35
|
/** tmp-file + rename so the SSE watcher and concurrent readers never observe a half-written file. */
|
|
36
36
|
export function writeJsonAtomic(file, data) {
|
|
37
|
-
|
|
38
|
-
fs.writeFileSync(tmp, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
39
|
-
for (let attempt = 0;; attempt++) {
|
|
40
|
-
try {
|
|
41
|
-
fs.renameSync(tmp, file);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
catch (err) {
|
|
45
|
-
const code = err.code;
|
|
46
|
-
if (attempt >= 10 || (code !== 'EPERM' && code !== 'EBUSY')) {
|
|
47
|
-
fs.rmSync(tmp, { force: true });
|
|
48
|
-
throw err;
|
|
49
|
-
}
|
|
50
|
-
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
37
|
+
writeJsonAtomicSync(file, data);
|
|
53
38
|
}
|
|
54
39
|
/** Moves a blocked task back to pending so it can be dispatched again. False when it was not blocked. */
|
|
55
40
|
export async function unblockTask(root, taskId, note) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Benchmarks view styles.
|
|
2
2
|
export const benchmarksCss = String.raw `/* Benchmarks */
|
|
3
|
-
.hero { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 20px 32px; padding: 22px; margin-bottom:
|
|
3
|
+
.hero { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 20px 32px; padding: 22px 0; margin-bottom: 0; background: none; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; box-shadow: none; transition: opacity .2s; }
|
|
4
4
|
.hero.is-running { opacity: .6; }
|
|
5
5
|
.eyebrow { margin: 0; font-size: 12px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
|
|
6
6
|
.hero-value { display: flex; align-items: center; gap: 10px; margin: 6px 0; font-size: 40px; font-weight: 650; letter-spacing: -0.03em; line-height: 1.1; font-variant-numeric: tabular-nums; }
|
|
@@ -9,14 +9,17 @@ export const benchmarksCss = String.raw `/* Benchmarks */
|
|
|
9
9
|
.hero-stats { display: grid; grid-template-columns: repeat(4, minmax(96px, auto)); gap: 4px 28px; margin: 0; }
|
|
10
10
|
.hero-stats dt { font-size: 12.5px; color: var(--muted); }
|
|
11
11
|
.hero-stats dd { margin: 0; font-size: 18px; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
12
|
-
.scenario-grid { display: grid; grid-template-columns:
|
|
13
|
-
.scenario { display: flex; flex-direction: column; gap: 10px; min-width: 0; padding:
|
|
14
|
-
.scenario
|
|
12
|
+
.scenario-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; }
|
|
13
|
+
.scenario { display: flex; flex-direction: column; gap: 10px; min-width: 0; padding: 12px 0; background: none; border: 0; border-bottom: 1px solid var(--line); border-radius: 0; box-shadow: none; transition: background .15s; }
|
|
14
|
+
.scenario:hover { background: var(--surface-2); }
|
|
15
|
+
.scenario.is-idle { color: var(--fg-3); }
|
|
15
16
|
.scenario-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
|
|
16
17
|
.scenario-head h3 { margin: 0; font-size: 14.5px; font-weight: 600; }
|
|
17
18
|
.scenario-sub { margin: -6px 0 0; font-size: 13px; color: var(--muted); }
|
|
18
19
|
.scenario p { margin: 0; font-size: 13.5px; line-height: 1.5; }
|
|
19
|
-
.metrics { display:
|
|
20
|
+
.metrics { display: inline-flex; font-variant-numeric: tabular-nums; }
|
|
21
|
+
.metrics > * { padding: 0 16px; border-left: 1px solid var(--line); }
|
|
22
|
+
.metrics > :first-child { padding-left: 0; border-left: 0; }
|
|
20
23
|
.metric-v { display: block; font-size: 17px; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
21
24
|
.metric-l { display: block; font-size: 12px; color: var(--muted); }
|
|
22
25
|
.details { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 4px 12px; margin: 0; padding-top: 10px; font-size: 12.5px; border-top: 1px solid var(--border); }
|
|
@@ -116,7 +116,7 @@ export const databaseBrowserScript = String.raw ` // ─── Database Studio:
|
|
|
116
116
|
var start = res.totalCount === 0 ? 0 : (res.page - 1) * res.limit + 1;
|
|
117
117
|
var end = Math.min(res.page * res.limit, res.totalCount);
|
|
118
118
|
var pagination = '<div class="pagination-bar">' +
|
|
119
|
-
'<div>Showing ' + start + '
|
|
119
|
+
'<div>Showing ' + start + '-' + end + ' of ' + res.totalCount + ' ' + (isColl ? 'documents' : 'records') + '</div>' +
|
|
120
120
|
'<div class="pagination-ctrls">' +
|
|
121
121
|
'<button class="btn small" type="button" id="btn-page-prev"' + (res.page <= 1 ? ' disabled' : '') + '>Previous</button>' +
|
|
122
122
|
'<span>Page ' + res.page + ' of ' + Math.max(1, res.totalPages) + '</span>' +
|
|
@@ -148,7 +148,7 @@ export const databaseConnectionScript = String.raw ` // ─── Database Stud
|
|
|
148
148
|
out.className = 'result err';
|
|
149
149
|
out.textContent = 'Error: ' + (r.error || 'Connection failed');
|
|
150
150
|
var hint = hintFor(r.error);
|
|
151
|
-
if (hint) out.textContent += '
|
|
151
|
+
if (hint) out.textContent += ': ' + hint;
|
|
152
152
|
return false;
|
|
153
153
|
}, function (e) {
|
|
154
154
|
out.className = 'result err'; out.textContent = 'Error: ' + e.message; return false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Database Studio data loading and shell.
|
|
2
|
-
export const databaseDataScript = String.raw ` // ─── View 5: Database Studio
|
|
2
|
+
export const databaseDataScript = String.raw ` // ─── View 5: Database Studio - data loading ────────────────────────────────
|
|
3
3
|
function load(fresh) {
|
|
4
4
|
state.loading = true;
|
|
5
5
|
renderDb();
|
|
@@ -63,7 +63,7 @@ export const databaseDataScript = String.raw ` // ─── View 5: Database St
|
|
|
63
63
|
? ' <span class="badge badge-source" title="Detected in ' + esc(s.exampleFile || '.env.example') + '">via ' + esc(s.sourceKey) + '</span>'
|
|
64
64
|
: (s.sourceKey ? ' <span class="badge badge-source">via ' + esc(s.sourceKey) + '</span>' : '');
|
|
65
65
|
return '<span class="pill" title="' + esc(s.maskedUrl) + '"><span class="dot on"></span><b>' + label + ':</b> ' +
|
|
66
|
-
esc(ENGINE_LABEL[s.engine] || s.engine) + (s.database ? ' · ' + esc(s.database) : '') + ' (' + Math.round(s.pingMs) + 'ms)
|
|
66
|
+
esc(ENGINE_LABEL[s.engine] || s.engine) + (s.database ? ' · ' + esc(s.database) : '') + ' (' + Math.round(s.pingMs) + 'ms): ' +
|
|
67
67
|
entityCount(s) + ' ' + entityNoun(s, true) + sourceBadge + ' <span class="badge badge-online">ONLINE</span></span>';
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -119,7 +119,7 @@ export const databaseDataScript = String.raw ` // ─── View 5: Database St
|
|
|
119
119
|
: ((state.schema[activeEnv + 'Tables'] || []).length);
|
|
120
120
|
$('count-data').textContent = String(dataCount);
|
|
121
121
|
var d = state.diff && state.diff.summary;
|
|
122
|
-
$('count-drift').textContent = d ? String(d.addedTablesCount + d.alteredTablesCount + d.droppedTablesCount) : '
|
|
122
|
+
$('count-drift').textContent = d ? String(d.addedTablesCount + d.alteredTablesCount + d.droppedTablesCount) : '-';
|
|
123
123
|
var script = state.diff ? generateScript(state.diff) : { text: '', lang: 'sql', count: 0 };
|
|
124
124
|
state.sql = script.text;
|
|
125
125
|
state.scriptLang = script.lang;
|
|
@@ -154,8 +154,8 @@ export const databaseDataScript = String.raw ` // ─── View 5: Database St
|
|
|
154
154
|
return '<div class="sr-only">Loading database schema…</div><div class="panes" aria-busy="true">' + col + col + '</div>';
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
var DB_ICON = '<svg class="icon empty-icon" width="56" height="56" viewBox="0 0
|
|
158
|
-
'<
|
|
157
|
+
var DB_ICON = '<svg class="icon empty-icon" width="56" height="56" viewBox="0 0 256 256" fill="currentColor" aria-hidden="true">' +
|
|
158
|
+
'<path d="M128,24C74.17,24,32,48.6,32,80v96c0,31.4,42.17,56,96,56s96-24.6,96-56V80C224,48.6,181.83,24,128,24Zm80,104c0,9.62-7.88,19.43-21.61,26.92C170.93,163.35,150.19,168,128,168s-42.93-4.65-58.39-13.08C55.88,147.43,48,137.62,48,128V111.36c17.06,15,46.23,24.64,80,24.64s62.94-9.68,80-24.64ZM69.61,53.08C85.07,44.65,105.81,40,128,40s42.93,4.65,58.39,13.08C200.12,60.57,208,70.38,208,80s-7.88,19.43-21.61,26.92C170.93,115.35,150.19,120,128,120s-42.93-4.65-58.39-13.08C55.88,99.43,48,89.62,48,80S55.88,60.57,69.61,53.08ZM186.39,202.92C170.93,211.35,150.19,216,128,216s-42.93-4.65-58.39-13.08C55.88,195.43,48,185.62,48,176V159.36c17.06,15,46.23,24.64,80,24.64s62.94-9.68,80-24.64V176C208,185.62,200.12,195.43,186.39,202.92Z"/></svg>';
|
|
159
159
|
|
|
160
160
|
function emptyState(title, text) {
|
|
161
161
|
return '<div class="empty card">' + DB_ICON + '<h3>' + esc(title) + '</h3><p>' + esc(text) + '</p>' +
|
|
@@ -15,7 +15,7 @@ export const databaseDriftScript = String.raw ` // ─── Database Studio: d
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
function colCell(c) {
|
|
18
|
-
if (!c) return '<span class="muted"
|
|
18
|
+
if (!c) return '<span class="muted">-</span>';
|
|
19
19
|
return esc(c.type) + (c.nullable ? '' : ' NOT NULL') + (c.default != null ? ' <span class="muted">DEFAULT</span> ' + esc(c.default) : '') +
|
|
20
20
|
(c.primaryKey ? ' <span class="badge badge-pk">PK</span>' : '') + (c.isMedia ? ' <span class="badge badge-media">IMAGE</span>' : '') +
|
|
21
21
|
(c.isSubcollection ? ' <span class="badge badge-subcol">SUBCOLLECTION</span>' : '');
|
|
@@ -32,9 +32,9 @@ export const databaseExplorerScript = String.raw ` // ─── Database Studio
|
|
|
32
32
|
mediaBadge(t, c);
|
|
33
33
|
var typeCell = esc(c.type) + (c.observedTypes ? ' <span class="muted">(' + esc(c.observedTypes.join(' | ')) + ')</span>' : '');
|
|
34
34
|
var lastCell = coll
|
|
35
|
-
? (c.presence != null ? Math.round(c.presence * 100) + '%' : '<span class="muted"
|
|
35
|
+
? (c.presence != null ? Math.round(c.presence * 100) + '%' : '<span class="muted">-</span>')
|
|
36
36
|
: (c.default == null ? '<span class="muted">NULL</span>' : esc(c.default));
|
|
37
|
-
return '<tr><td>' + (tags || '<span class="muted"
|
|
37
|
+
return '<tr><td>' + (tags || '<span class="muted">-</span>') + '</td><td class="mono">' + esc(c.name) +
|
|
38
38
|
(fk ? ' <span class="muted">→ ' + esc(fk.referencedTable + '.' + fk.referencedColumn) + '</span>' : '') + '</td><td class="mono">' + typeCell +
|
|
39
39
|
'</td><td>' + (c.nullable ? 'YES' : '<b>NO</b>') + '</td><td class="mono">' + lastCell + '</td></tr>';
|
|
40
40
|
}).join('');
|
|
@@ -6,7 +6,7 @@ export const databaseCss = String.raw `/* Database Studio */
|
|
|
6
6
|
.diagnostic-banner { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 16px; margin: 12px 0 16px; background: var(--danger-bg); border: 1px solid var(--danger-line); border-radius: var(--r-control); }
|
|
7
7
|
.diagnostic-content { display: flex; align-items: center; gap: 10px; font-size: 13.5px; }
|
|
8
8
|
.diagnostic-tag { font-family: var(--mono); font-size: 11px; font-weight: 700; color: var(--danger); background: var(--surface); border: 1px solid var(--danger-line); padding: 0 7px; border-radius: var(--r-control); }
|
|
9
|
-
.diagnostic-msg code { background: var(--surface); padding: 1px 6px; border-radius:
|
|
9
|
+
.diagnostic-msg code { background: var(--surface); padding: 1px 6px; border-radius: var(--r-control); font-family: var(--mono); }
|
|
10
10
|
.diagnostic-actions { display: flex; gap: 8px; }
|
|
11
11
|
.template-helper-box { background: var(--active-bg); border: 1px solid var(--active-line); border-radius: var(--r-control); padding: 12px 14px; margin-bottom: 16px; }
|
|
12
12
|
.template-helper-label { font-size: 12.5px; color: var(--muted); margin-bottom: 8px; }
|
|
@@ -18,7 +18,7 @@ export const databaseCss = String.raw `/* Database Studio */
|
|
|
18
18
|
.chip-dot.on { background: var(--success); }
|
|
19
19
|
.chip-dot.off { background: var(--danger); }
|
|
20
20
|
|
|
21
|
-
.thumb-pop { position: fixed; z-index: 60; width: 220px; padding: 8px; border-radius: var(--r-card); border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow
|
|
21
|
+
.thumb-pop { position: fixed; z-index: 60; width: 220px; padding: 8px; border-radius: var(--r-card); border: 1px solid var(--border); background: var(--surface); box-shadow: var(--shadow); pointer-events: none; }
|
|
22
22
|
.thumb-box { display: grid; place-items: center; min-height: 120px; max-height: 200px; overflow: hidden; border-radius: var(--r-control); background: repeating-conic-gradient(var(--chip) 0% 25%, var(--surface) 0% 50%) 50% / 16px 16px; }
|
|
23
23
|
.thumb-box img { max-width: 100%; max-height: 200px; display: block; }
|
|
24
24
|
.thumb-box .ph { color: var(--muted); font-size: 12px; text-align: center; padding: 12px; }
|
|
@@ -77,7 +77,7 @@ pre.sql { margin: 0; padding: 16px; overflow: auto; max-height: 70vh; background
|
|
|
77
77
|
.sql .p { color: var(--active); } .sql .n { color: var(--active); font-weight: 600; } .sql .o { color: var(--primary); }
|
|
78
78
|
.lang { font-family: var(--mono); font-size: 11px; padding: 0 7px; border-radius: var(--r-control); border: 1px solid var(--primary-line); background: var(--primary-tint); color: var(--primary); margin-right: 6px; }
|
|
79
79
|
.copy-wrap { position: relative; display: inline-flex; }
|
|
80
|
-
.tooltip { position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: var(--success); color:
|
|
80
|
+
.tooltip { position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); background: var(--success); color: var(--accent-fg); font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: var(--r-control); pointer-events: none; opacity: 0; transition: opacity .15s; white-space: nowrap; }
|
|
81
81
|
.tooltip.show { opacity: 1; }
|
|
82
82
|
|
|
83
83
|
.empty { text-align: center; padding: 48px 24px; }
|
|
@@ -87,7 +87,7 @@ pre.sql { margin: 0; padding: 16px; overflow: auto; max-height: 70vh; background
|
|
|
87
87
|
|
|
88
88
|
.env-toggle-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; margin: 14px 0 6px; }
|
|
89
89
|
.env-segmented { display: inline-flex; gap: 2px; padding: 3px; background: var(--chip); border: 1px solid var(--border); border-radius: var(--r-control); }
|
|
90
|
-
.env-seg-btn { border: 1px solid transparent; background: transparent; padding: 4px 14px; border-radius:
|
|
90
|
+
.env-seg-btn { border: 1px solid transparent; background: transparent; padding: 4px 14px; border-radius: var(--r-control); cursor: pointer; color: var(--muted); font-size: 13px; font-weight: 600; transition: all .15s ease; }
|
|
91
91
|
.env-seg-btn:hover { color: var(--text); }
|
|
92
92
|
.env-seg-btn[aria-pressed="true"] { background: var(--surface); color: var(--primary); border-color: var(--border); box-shadow: var(--shadow); }
|
|
93
93
|
.env-seg-btn[data-env-mode="prod"][aria-pressed="true"] { color: var(--danger); background: var(--danger-bg); border-color: var(--danger-line); }
|
|
@@ -112,7 +112,7 @@ table.data-table tr:hover td { background: var(--bg); }
|
|
|
112
112
|
.td-actions { display: flex; gap: 6px; align-items: center; white-space: nowrap; }
|
|
113
113
|
.pagination-bar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 4px; font-size: 13px; color: var(--muted); }
|
|
114
114
|
.pagination-ctrls { display: flex; align-items: center; gap: 8px; }
|
|
115
|
-
.cell-preview-btn { padding: 1px 6px; font-size: 11px; border-radius:
|
|
115
|
+
.cell-preview-btn { padding: 1px 6px; font-size: 11px; border-radius: var(--r-control); border: 1px solid var(--primary-line); color: var(--primary); background: var(--primary-tint); cursor: pointer; }
|
|
116
116
|
.cell-preview-btn:hover { background: var(--surface); }
|
|
117
117
|
|
|
118
118
|
`;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Flow page styles, all scoped under #panel-flow.
|
|
2
|
-
export const flowCss = String.raw `#panel-flow .seg { display: inline-flex; border: 1px solid var(--line); border-radius:
|
|
2
|
+
export const flowCss = String.raw `#panel-flow .seg { display: inline-flex; border: 1px solid var(--line); border-radius: var(--r-control); overflow: hidden; background: var(--surface); }
|
|
3
3
|
#panel-flow .seg button { border: 0; background: none; padding: 0 12px; height: 30px; font-size: 13px; color: var(--fg-2); cursor: pointer; }
|
|
4
4
|
#panel-flow .seg button + button { border-left: 1px solid var(--line); }
|
|
5
5
|
#panel-flow .seg button[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 500; }
|
|
6
|
-
#panel-flow .fl-panel { background: var(--surface); border: 1px solid var(--line); border-radius:
|
|
6
|
+
#panel-flow .fl-panel { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-card); min-width: 0; overflow: hidden; }
|
|
7
7
|
#panel-flow .fl-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px 14px; padding: 12px 16px; }
|
|
8
8
|
#panel-flow .fl-bar .right { margin-left: auto; display: flex; gap: 14px; align-items: center; flex-wrap: wrap; font-size: 12.5px; color: var(--fg-3); }
|
|
9
9
|
#panel-flow .flow-wrap { overflow: auto; max-height: 72vh; position: relative; border-top: 1px solid var(--line); }
|
|
@@ -18,7 +18,7 @@ export const flowCss = String.raw `#panel-flow .seg { display: inline-flex; bord
|
|
|
18
18
|
#panel-flow .lane-label { position: sticky; left: 0; width: 128px; padding: 10px 12px; display: flex; flex-direction: column; gap: 2px; background: var(--surface); border-right: 1px solid var(--line); height: 100%; z-index: 2; }
|
|
19
19
|
#panel-flow .lane-label .faint { font-size: 11.5px; }
|
|
20
20
|
#panel-flow .round-head { position: absolute; top: 0; height: 30px; display: flex; align-items: center; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--fg-3); }
|
|
21
|
-
#panel-flow .node { position: absolute; display: flex; flex-direction: column; justify-content: center; gap: 3px; padding: 7px 10px; background: var(--surface); border: 1px solid var(--line); border-radius:
|
|
21
|
+
#panel-flow .node { position: absolute; display: flex; flex-direction: column; justify-content: center; gap: 3px; padding: 7px 10px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-control); text-align: left; cursor: pointer; transition: opacity .15s, border-color .15s, box-shadow .15s; z-index: 1; }
|
|
22
22
|
#panel-flow .node:hover { border-color: var(--line-2); box-shadow: var(--shadow); }
|
|
23
23
|
#panel-flow .node.in_progress { border-color: color-mix(in srgb, var(--run) 50%, var(--line)); background: color-mix(in srgb, var(--run-soft) 55%, var(--surface)); }
|
|
24
24
|
#panel-flow .node.blocked { border-color: color-mix(in srgb, var(--bad) 50%, var(--line)); background: color-mix(in srgb, var(--bad-soft) 55%, var(--surface)); }
|