@njeybe/nativ 2.4.1 → 2.5.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 +20 -0
- package/README.md +26 -0
- package/dist/commands/db/db-formatter.d.ts +5 -0
- package/dist/commands/db/db-formatter.js +116 -0
- package/dist/commands/db/db-introspect.d.ts +11 -0
- package/dist/commands/db/db-introspect.js +84 -0
- package/dist/commands/db.js +4 -206
- package/dist/commands/profile.d.ts +3 -0
- package/dist/commands/profile.js +39 -0
- package/dist/commands/task/task-add.d.ts +11 -0
- package/dist/commands/task/task-add.js +169 -0
- package/dist/commands/task/task-common.d.ts +57 -0
- package/dist/commands/task/task-common.js +45 -0
- package/dist/commands/task/task-escalate.d.ts +7 -0
- package/dist/commands/task/task-escalate.js +119 -0
- package/dist/commands/task/task-lifecycle.d.ts +5 -0
- package/dist/commands/task/task-lifecycle.js +252 -0
- package/dist/commands/task/task-query.d.ts +10 -0
- package/dist/commands/task/task-query.js +269 -0
- package/dist/commands/task.d.ts +9 -87
- package/dist/commands/task.js +8 -850
- package/dist/commands/triage/triage-store.d.ts +56 -0
- package/dist/commands/triage/triage-store.js +108 -0
- package/dist/commands/triage/triage-ui.d.ts +14 -0
- package/dist/commands/triage/triage-ui.js +104 -0
- package/dist/commands/triage.d.ts +3 -52
- package/dist/commands/triage.js +8 -216
- package/dist/core/enforcement.d.ts +1 -0
- package/dist/core/enforcement.js +19 -0
- package/dist/core/human-gate.d.ts +2 -1
- package/dist/core/human-gate.js +9 -2
- package/dist/core/profiles.d.ts +14 -0
- package/dist/core/profiles.js +82 -0
- package/dist/core/setup/config-merger.d.ts +59 -0
- package/dist/core/setup/config-merger.js +107 -0
- package/dist/core/setup/session-context.d.ts +4 -0
- package/dist/core/setup/session-context.js +65 -0
- package/dist/core/setup/setup-planner.d.ts +14 -0
- package/dist/core/setup/setup-planner.js +180 -0
- package/dist/core/setup/template-planner.d.ts +8 -0
- package/dist/core/setup/template-planner.js +32 -0
- package/dist/core/setup/types.d.ts +65 -0
- package/dist/core/setup/types.js +55 -0
- package/dist/core/setup-assets.d.ts +8 -119
- package/dist/core/setup-assets.js +7 -437
- package/dist/core/tier1-liaison.d.ts +2 -0
- package/dist/core/tier1-liaison.js +64 -3
- package/dist/core/verifier/executor.d.ts +5 -0
- package/dist/core/verifier/executor.js +62 -0
- package/dist/core/verifier/phases.d.ts +5 -0
- package/dist/core/verifier/phases.js +71 -0
- package/dist/core/verifier/task-verifier.d.ts +37 -0
- package/dist/core/verifier/task-verifier.js +233 -0
- package/dist/core/verifier/types.d.ts +51 -0
- package/dist/core/verifier/types.js +1 -0
- package/dist/core/verifier.d.ts +5 -93
- package/dist/core/verifier.js +5 -359
- package/dist/db/introspector.js +59 -59
- package/dist/governor/evaluator.d.ts +4 -64
- package/dist/governor/evaluator.js +21 -686
- package/dist/governor/patch-applier.d.ts +8 -0
- package/dist/governor/patch-applier.js +121 -0
- package/dist/governor/self-healing.d.ts +24 -0
- package/dist/governor/self-healing.js +351 -0
- package/dist/governor/test-integrity.d.ts +41 -0
- package/dist/governor/test-integrity.js +237 -0
- package/dist/index.js +8 -0
- package/dist/mcp/capture.d.ts +4 -0
- package/dist/mcp/capture.js +33 -0
- package/dist/mcp/resources.d.ts +39 -0
- package/dist/mcp/resources.js +44 -0
- package/dist/mcp/server.d.ts +11 -0
- package/dist/mcp/server.js +14 -89
- package/dist/mcp/tools.d.ts +1 -0
- package/dist/mcp/tools.js +1 -0
- package/dist/runner/agent-supervisor.d.ts +8 -302
- package/dist/runner/agent-supervisor.js +41 -1030
- package/dist/runner/cli-config.d.ts +84 -0
- package/dist/runner/cli-config.js +215 -0
- package/dist/runner/native-driver.d.ts +18 -0
- package/dist/runner/native-driver.js +208 -0
- package/dist/runner/native-engine.d.ts +27 -0
- package/dist/runner/native-engine.js +184 -0
- package/dist/runner/native-tools.d.ts +31 -0
- package/dist/runner/native-tools.js +241 -0
- package/dist/runner/process-utils.d.ts +12 -0
- package/dist/runner/process-utils.js +96 -0
- package/dist/runner/run-lifecycle.d.ts +6 -0
- package/dist/runner/run-lifecycle.js +42 -0
- package/dist/runner/types.d.ts +215 -0
- package/dist/runner/types.js +41 -0
- package/dist/scanner/detector.js +42 -663
- package/dist/scanner/detectors/ecosystem-detector.d.ts +16 -0
- package/dist/scanner/detectors/ecosystem-detector.js +354 -0
- package/dist/scanner/detectors/orm-detector.d.ts +5 -0
- package/dist/scanner/detectors/orm-detector.js +59 -0
- package/dist/scanner/detectors/workspace-detector.d.ts +19 -0
- package/dist/scanner/detectors/workspace-detector.js +266 -0
- package/dist/server/cli-runner.d.ts +15 -0
- package/dist/server/cli-runner.js +43 -0
- package/dist/server/event-hub.d.ts +42 -0
- package/dist/server/event-hub.js +169 -0
- package/dist/server/http-utils.d.ts +15 -0
- package/dist/server/http-utils.js +56 -0
- package/dist/server/plan-utils.d.ts +9 -0
- package/dist/server/plan-utils.js +31 -0
- package/dist/server/routes/db-routes.d.ts +57 -0
- package/dist/server/routes/db-routes.js +484 -0
- package/dist/server/routes/task-routes.d.ts +266 -0
- package/dist/server/routes/task-routes.js +285 -0
- package/dist/server/routes/telemetry-routes.d.ts +128 -0
- package/dist/server/routes/telemetry-routes.js +179 -0
- package/dist/server/routes/triage-routes.d.ts +81 -0
- package/dist/server/routes/triage-routes.js +244 -0
- package/dist/server/routes/worktree-routes.d.ts +31 -0
- package/dist/server/routes/worktree-routes.js +144 -0
- package/dist/server/studio-server.d.ts +4 -11
- package/dist/server/studio-server.js +34 -1578
- package/dist/server/studio-ui/shell/pipeline-state.js +2 -0
- package/dist/server/studio-ui/shell/triage.js +10 -0
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/agents/architect.md +4 -2
- package/plugin/agents/verifier.md +1 -1
- package/plugin/agents/worker.md +7 -4
- package/plugin/skills/nativ/SKILL.md +1 -0
- package/templates/.claudeignore +62 -45
- package/templates/AGENTS.md +1 -0
- package/templates/CLAUDE.md +13 -0
- package/templates/claude-agents/architect.md +4 -2
- package/templates/claude-agents/verifier.md +1 -1
- package/templates/claude-agents/worker.md +7 -4
- 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/dist/commands/session.d.ts +0 -22
- package/dist/commands/session.js +0 -106
- package/dist/core/session-memory.d.ts +0 -42
- package/dist/core/session-memory.js +0 -121
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.5.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Adaptive Ceremony Profiles.** Introduced `nativ profile [prototype | solo | enterprise]` to adapt framework rigor across all development stages:
|
|
8
|
+
- **`prototype`**: Zero-friction exploration for hackathons and day 1 greenfield projects. Relaxes scope enforcement, enables contract auto-adoption without escalation halts, and disables test integrity blocking.
|
|
9
|
+
- **`solo`**: Lightweight agility for solo microservices and utilities with advisory warnings and auto-evolving contracts.
|
|
10
|
+
- **`enterprise`**: Full production rigor with strict role boundaries, formal contract governor review, test integrity protection, and circuit breakers.
|
|
11
|
+
- **Native CI/CD Headless Automation.** `refuseHeadless` now detects CI environments (`CI`, `GITHUB_ACTIONS`, `GITLAB_CI`, `NATIV_CI_OVERRIDE`) to permit headless test verification and automated pipeline gates without TTY failures.
|
|
12
|
+
|
|
13
|
+
### Improved & Refactored
|
|
14
|
+
|
|
15
|
+
- **Modular Architecture.** Decomposed large monolithic modules into focused, single-responsibility submodules adhering to the ~300-line standard:
|
|
16
|
+
- `src/commands/task/`: Split into `task-query`, `task-lifecycle`, `task-add`, `task-escalate`, and `task-common`.
|
|
17
|
+
- `src/core/verifier/`: Split into `executor`, `phases`, and `task-verifier`.
|
|
18
|
+
- `src/core/setup/`: Split into `types`, `config-merger`, `template-planner`, `setup-planner`, and `session-context`.
|
|
19
|
+
- `src/commands/db/`: Decoupled database introspection (`db-introspect`) and ANSI terminal presentation (`db-formatter`).
|
|
20
|
+
- `src/commands/triage/`: Decoupled triage persistence (`triage-store`) and interactive card UI (`triage-ui`).
|
|
21
|
+
- **ESM Type Re-exports.** Fixed TypeScript interface re-exports in barrel files using explicit `export type` syntax.
|
|
22
|
+
|
|
3
23
|
## 2.4.1
|
|
4
24
|
|
|
5
25
|
### Fixed & Improved
|
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ cd your-project && nativ init # scaffold .ai/ and the Claude Code configurati
|
|
|
34
34
|
- **Contracts, not chat.** `.ai/db_schema.json`, `.ai/api_contracts.json` and `.ai/ui_specs.md` are the source of truth. Workers build against them and escalate gaps instead of improvising.
|
|
35
35
|
- **Boundaries enforced by code.** A Claude Code hook checks every file write against the active task, the protected contracts and your secret files. It does not rely on the model remembering a rule.
|
|
36
36
|
- **Independent verification.** A gatekeeper re-runs each task's checks before it can complete, and a separate verifier agent reviews the result.
|
|
37
|
+
- **Adaptive ceremony.** Shift seamlessly between `prototype` velocity, balanced `solo` defaults, and strict `enterprise` compliance (`nativ profile`).
|
|
37
38
|
- **A project that remembers.** Settled escalations and human-approved lessons travel with later tasks, so the same gap is not raised twice.
|
|
38
39
|
- **Zero-credential air-gap.** Agents see database structure, never passwords, connection strings or rows.
|
|
39
40
|
|
|
@@ -141,6 +142,7 @@ Every command takes an optional `[targetDir]` (default: the current directory) a
|
|
|
141
142
|
| :--- | :--- | :--- |
|
|
142
143
|
| [`init`](#nativ-init) | Scaffold `.ai/` and the Claude Code configuration | You |
|
|
143
144
|
| [`setup`](#nativ-setup) | Write or merge the Claude Code configuration | You |
|
|
145
|
+
| [`profile`](#nativ-profile) | Inspect or switch ceremony profile (`prototype`, `solo`, `enterprise`) | You, Agent |
|
|
144
146
|
| [`doctor`](#nativ-doctor) | Check the integration and the harness; `--fix` repairs | You, Agent (read-only) |
|
|
145
147
|
| [`update`](#nativ-update) | Refresh directives, role guides and agents to the latest templates | You |
|
|
146
148
|
| [`validate`](#nativ-validate) | Check the contracts and role guides are well formed | You, Agent |
|
|
@@ -203,6 +205,28 @@ nativ setup [targetDir] [options]
|
|
|
203
205
|
| `-f, --force` | Also replace agent and directive files you edited |
|
|
204
206
|
| `--json` | Output the result as JSON |
|
|
205
207
|
|
|
208
|
+
#### `nativ profile`
|
|
209
|
+
|
|
210
|
+
Inspect or switch the active ceremony profile. Profiles adapt the rigor of the gatekeeper, governor, and enforcement hooks to the stage of your project:
|
|
211
|
+
|
|
212
|
+
- `prototype`: Maximum velocity for hackathons and spikes. Auto-adopts additive contract patches, relaxes test-integrity guards, and warns on style/scope violations.
|
|
213
|
+
- `solo`: Balanced defaults for solo developers and small teams (default). Enforces task file scope, requires verification commands, but uses lightweight reviews.
|
|
214
|
+
- `enterprise`: Maximum governance for high-stakes production code. Strict blocking enforcement, mandatory verification phases, required test integrity, and strict human review.
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
nativ profile [profile] [targetDir] [options]
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
nativ profile # Show current profile and resolved settings
|
|
222
|
+
nativ profile solo # Switch to the solo profile
|
|
223
|
+
nativ profile enterprise --json # Switch and return config as JSON
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
| Option | Description |
|
|
227
|
+
| :--- | :--- |
|
|
228
|
+
| `--json` | Output current or updated profile configuration as JSON |
|
|
229
|
+
|
|
206
230
|
#### `nativ doctor`
|
|
207
231
|
|
|
208
232
|
Check the integration for drift (MCP server, hooks, permissions, agents, providers) and the harness itself: settings that switch the guardrails off (`disableAllHooks`, `bypassPermissions`), credentials in files agents read (file and line only), roles with no guide, and lessons waiting for approval.
|
|
@@ -620,6 +644,7 @@ Written by `setup`; every key is optional.
|
|
|
620
644
|
|
|
621
645
|
```json
|
|
622
646
|
{
|
|
647
|
+
"profile": "solo",
|
|
623
648
|
"enforcement": "warn",
|
|
624
649
|
"workerModels": { "simple": "haiku", "standard": "sonnet", "complex": "opus" },
|
|
625
650
|
"verifyPhases": [{ "name": "types", "run": "npx tsc --noEmit" }],
|
|
@@ -631,6 +656,7 @@ Written by `setup`; every key is optional.
|
|
|
631
656
|
|
|
632
657
|
| Key | Default | Meaning |
|
|
633
658
|
| :--- | :--- | :--- |
|
|
659
|
+
| `profile` | `solo` | Active ceremony profile: `prototype`, `solo` or `enterprise`. See [`nativ profile`](#nativ-profile) |
|
|
634
660
|
| `enforcement` | `warn` | Role enforcement mode: `warn`, `block` or `off`. See [Guardrails](#guardrails) |
|
|
635
661
|
| `workerModels` | as above | Model per task `complexity`, returned as `recommendedModel` |
|
|
636
662
|
| `verifyPhases` | none | Checks run before every task's own command. See [Verification](#verification) |
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { DatabaseEnv, DatabaseStatus, SchemaDiff, TableSchema } from '../../db/types.js';
|
|
2
|
+
export declare function printStatusLine(env: DatabaseEnv, s: DatabaseStatus): void;
|
|
3
|
+
export declare function describeColumn(c: TableSchema['columns'][number]): string;
|
|
4
|
+
export declare function printDiff(diff: SchemaDiff, targetLabel: string): void;
|
|
5
|
+
export declare function printInspectTables(env: DatabaseEnv, maskedUrl: string, tables: TableSchema[], entityWord: string, isCollection: boolean): void;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { ENGINE_LABEL, keyFamily } from './db-introspect.js';
|
|
3
|
+
export function printStatusLine(env, s) {
|
|
4
|
+
const label = pc.bold(env.toUpperCase().padEnd(4));
|
|
5
|
+
const synthBadge = s.synthesized && s.sourceKey ? ` ${pc.magenta(`[synthesized from ${keyFamily(s.sourceKey)}]`)}` : '';
|
|
6
|
+
if (!s.connected) {
|
|
7
|
+
console.log(` ${pc.red('●')} ${label} ${pc.red('[OFFLINE]')}${synthBadge} ${pc.dim(s.error ?? 'Unknown error')}`);
|
|
8
|
+
if (s.suggestion) {
|
|
9
|
+
const isDiagnostic = /Ensure (MySQL|the MySQL|the PostgreSQL)/.test(s.suggestion);
|
|
10
|
+
console.log(` ${pc.yellow(isDiagnostic ? `[DIAGNOSTIC] ${s.suggestion}` : `Tip: ${s.suggestion}`)}`);
|
|
11
|
+
}
|
|
12
|
+
if (s.maskedUrl)
|
|
13
|
+
console.log(pc.dim(` ${s.maskedUrl}`));
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const count = s.entityCount ?? s.tableCount;
|
|
17
|
+
const isCollection = s.entityType === 'collection';
|
|
18
|
+
const entityWord = isCollection ? (count === 1 ? 'collection' : 'collections') : (count === 1 ? 'table' : 'tables');
|
|
19
|
+
const sourceInfo = synthBadge
|
|
20
|
+
? synthBadge
|
|
21
|
+
: s.detectedFromExample && s.sourceKey
|
|
22
|
+
? pc.cyan(` (via ${s.sourceKey} from ${s.exampleFile ?? '.env.example'})`)
|
|
23
|
+
: s.sourceKey
|
|
24
|
+
? pc.dim(` (via ${s.sourceKey})`)
|
|
25
|
+
: '';
|
|
26
|
+
console.log(` ${pc.green('●')} ${label} ${pc.green('[ONLINE]')} ${ENGINE_LABEL[s.engine] ?? s.engine}` +
|
|
27
|
+
`${s.database ? ` · ${s.database}` : ''} ${pc.dim(`(${s.pingMs}ms)`)} – ${count} ${entityWord}${sourceInfo}`);
|
|
28
|
+
console.log(pc.dim(` ${s.maskedUrl}`));
|
|
29
|
+
}
|
|
30
|
+
export function describeColumn(c) {
|
|
31
|
+
const flags = [];
|
|
32
|
+
if (c.primaryKey)
|
|
33
|
+
flags.push('PK');
|
|
34
|
+
if (c.isMedia)
|
|
35
|
+
flags.push('[IMAGE]');
|
|
36
|
+
if (c.isSubcollection)
|
|
37
|
+
flags.push('[SUBCOLLECTION]');
|
|
38
|
+
const flagStr = flags.length ? ` ${pc.magenta(flags.join(' '))}` : '';
|
|
39
|
+
return `${c.type}${c.nullable ? '' : ' NOT NULL'}${c.default != null ? ` DEFAULT ${c.default}` : ''}${flagStr}`;
|
|
40
|
+
}
|
|
41
|
+
export function printDiff(diff, targetLabel) {
|
|
42
|
+
const s = diff.summary;
|
|
43
|
+
console.log(pc.bold(pc.cyan(`\nSchema Drift: Dev → ${targetLabel}`)));
|
|
44
|
+
console.log(` ${pc.green(`+${s.addedTablesCount} added`)} ${pc.yellow(`~${s.alteredTablesCount} altered`)} ` +
|
|
45
|
+
`${pc.red(`-${s.droppedTablesCount} dropped`)} ${pc.dim(`${s.unchangedTablesCount} unchanged`)}`);
|
|
46
|
+
if (!s.addedTablesCount && !s.alteredTablesCount && !s.droppedTablesCount) {
|
|
47
|
+
console.log(pc.green(`\n✔ In sync: Dev matches ${targetLabel}.\n`));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
for (const t of diff.addedTables) {
|
|
51
|
+
const isCol = t.entityType === 'collection';
|
|
52
|
+
const tag = isCol ? '[NEW COLLECTION]' : '[NEW TABLE]';
|
|
53
|
+
const fieldWord = isCol ? 'fields' : 'columns';
|
|
54
|
+
console.log(`\n ${pc.green(tag)} ${pc.bold(t.name)} ${pc.dim(`(${t.columns.length} ${fieldWord})`)}`);
|
|
55
|
+
}
|
|
56
|
+
for (const t of diff.alteredTables) {
|
|
57
|
+
console.log(`\n ${pc.yellow('[ALTERED]')} ${pc.bold(t.name)}${t.isDestructive ? ` ${pc.red('[DESTRUCTIVE]')}` : ''}`);
|
|
58
|
+
for (const c of t.addedColumns)
|
|
59
|
+
console.log(pc.green(` + ${c.name}: ${describeColumn(c)}`));
|
|
60
|
+
for (const ch of t.alteredColumns) {
|
|
61
|
+
console.log(pc.yellow(` ~ ${ch.name} (${ch.changedFields.join(', ')}): ${describeColumn(ch.before)} → ${describeColumn(ch.after)}`));
|
|
62
|
+
}
|
|
63
|
+
for (const c of t.droppedColumns)
|
|
64
|
+
console.log(pc.red(` - ${c.name}: ${describeColumn(c)}`));
|
|
65
|
+
for (const i of t.addedIndexes)
|
|
66
|
+
console.log(pc.green(` + index ${i.name} (${i.columns.join(', ')})${i.unique ? ' unique' : ''}`));
|
|
67
|
+
for (const i of t.droppedIndexes)
|
|
68
|
+
console.log(pc.red(` - index ${i.name} (${i.columns.join(', ')})`));
|
|
69
|
+
for (const f of t.addedForeignKeys)
|
|
70
|
+
console.log(pc.green(` + FK ${f.column} → ${f.referencedTable}.${f.referencedColumn}`));
|
|
71
|
+
for (const f of t.droppedForeignKeys)
|
|
72
|
+
console.log(pc.red(` - FK ${f.column} → ${f.referencedTable}.${f.referencedColumn}`));
|
|
73
|
+
}
|
|
74
|
+
for (const t of diff.droppedTables) {
|
|
75
|
+
const isCol = t.entityType === 'collection';
|
|
76
|
+
const tag = isCol ? '[DROPPED COLLECTION]' : '[DROPPED]';
|
|
77
|
+
const fieldWord = isCol ? 'fields' : 'columns';
|
|
78
|
+
console.log(`\n ${pc.red(tag)} ${pc.bold(t.name)} ${pc.dim(`(${t.columns.length} ${fieldWord})`)}`);
|
|
79
|
+
}
|
|
80
|
+
if (s.hasDestructiveChanges) {
|
|
81
|
+
console.log(pc.bgRed(pc.white(pc.bold(' HIGH SEVERITY '))) +
|
|
82
|
+
pc.red(` Destructive changes detected against ${targetLabel}. Applying them would drop data or fail on existing rows.`));
|
|
83
|
+
}
|
|
84
|
+
console.log('');
|
|
85
|
+
}
|
|
86
|
+
export function printInspectTables(env, maskedUrl, tables, entityWord, isCollection) {
|
|
87
|
+
console.log(pc.bold(pc.cyan(`\n${env.toUpperCase()} Schema (${entityWord})`)) + pc.dim(` · ${maskedUrl}`));
|
|
88
|
+
for (const t of tables) {
|
|
89
|
+
const isCol = t.entityType === 'collection' || isCollection;
|
|
90
|
+
const fieldWord = isCol ? 'fields' : 'columns';
|
|
91
|
+
const tag = isCol ? pc.green('[COLLECTION] ') : '';
|
|
92
|
+
console.log(`\n ${tag}${pc.bold(t.name)} ${pc.dim(`(${t.columns.length} ${fieldWord})`)}`);
|
|
93
|
+
if (t.description)
|
|
94
|
+
console.log(pc.dim(` ${t.description}`));
|
|
95
|
+
const fkByColumn = new Map(t.foreignKeys.map((f) => [f.column.toLowerCase(), f]));
|
|
96
|
+
const nameWidth = Math.max(...t.columns.map((c) => c.name.length), 4);
|
|
97
|
+
for (const c of t.columns) {
|
|
98
|
+
const fk = fkByColumn.get(c.name.toLowerCase());
|
|
99
|
+
const flags = [];
|
|
100
|
+
if (c.primaryKey)
|
|
101
|
+
flags.push(pc.magenta('PK'));
|
|
102
|
+
if (c.isMedia)
|
|
103
|
+
flags.push(pc.magenta('[IMAGE]'));
|
|
104
|
+
if (c.isSubcollection)
|
|
105
|
+
flags.push(pc.cyan('[SUBCOLLECTION]'));
|
|
106
|
+
if (fk)
|
|
107
|
+
flags.push(pc.cyan(`FK→${fk.referencedTable}.${fk.referencedColumn}`));
|
|
108
|
+
const tags = flags.join(' ');
|
|
109
|
+
console.log(` ${c.name.padEnd(nameWidth)} ${c.type}${c.nullable ? '' : pc.dim(' NOT NULL')}` +
|
|
110
|
+
`${c.default != null ? pc.dim(` DEFAULT ${c.default}`) : ''}${tags ? ` ${tags}` : ''}`);
|
|
111
|
+
}
|
|
112
|
+
for (const i of t.indexes)
|
|
113
|
+
console.log(pc.dim(` index ${i.name} (${i.columns.join(', ')})${i.unique ? ' unique' : ''}`));
|
|
114
|
+
}
|
|
115
|
+
console.log('');
|
|
116
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DatabaseEnv, DatabaseStatus } from '../../db/types.js';
|
|
2
|
+
import { type IntrospectionResult } from '../../db/introspector.js';
|
|
3
|
+
export declare const ENGINE_LABEL: Record<string, string>;
|
|
4
|
+
/**
|
|
5
|
+
* Actionable hint when a local MySQL/PostgreSQL server refuses the connection, e.g. MySQL not started in
|
|
6
|
+
* the XAMPP Control Panel. Only host/port are read from the URL; credentials are never touched.
|
|
7
|
+
*/
|
|
8
|
+
export declare function localStackDiagnostic(url: string, engine: string, status: DatabaseStatus, fragmentedKeys?: string[]): string | null;
|
|
9
|
+
export declare function introspectEnv(targetDir: string, env: DatabaseEnv): Promise<IntrospectionResult>;
|
|
10
|
+
/** DB_HOST -> DB_*, PROD_DB_HOST -> PROD_DB_*, PGHOST -> PG*. */
|
|
11
|
+
export declare function keyFamily(key: string): string;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { resolveConnections } from '../../db/env-parser.js';
|
|
2
|
+
import { disconnectedStatus, introspectDatabase } from '../../db/introspector.js';
|
|
3
|
+
export const ENGINE_LABEL = {
|
|
4
|
+
postgresql: 'PostgreSQL',
|
|
5
|
+
mysql: 'MySQL',
|
|
6
|
+
sqlite: 'SQLite',
|
|
7
|
+
mongodb: 'MongoDB',
|
|
8
|
+
firestore: 'Firebase Firestore',
|
|
9
|
+
};
|
|
10
|
+
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]', '0.0.0.0']);
|
|
11
|
+
/**
|
|
12
|
+
* Actionable hint when a local MySQL/PostgreSQL server refuses the connection, e.g. MySQL not started in
|
|
13
|
+
* the XAMPP Control Panel. Only host/port are read from the URL; credentials are never touched.
|
|
14
|
+
*/
|
|
15
|
+
export function localStackDiagnostic(url, engine, status, fragmentedKeys) {
|
|
16
|
+
if (engine !== 'mysql' && engine !== 'postgresql')
|
|
17
|
+
return null;
|
|
18
|
+
if (!/ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|connection refused/i.test(status.error ?? ''))
|
|
19
|
+
return null;
|
|
20
|
+
let host = '';
|
|
21
|
+
let port = engine === 'mysql' ? 3306 : 5432;
|
|
22
|
+
try {
|
|
23
|
+
const parsed = new URL(url);
|
|
24
|
+
host = parsed.hostname;
|
|
25
|
+
if (parsed.port)
|
|
26
|
+
port = Number(parsed.port);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const label = ENGINE_LABEL[engine];
|
|
32
|
+
const isLocal = LOCAL_HOSTS.has(host);
|
|
33
|
+
const action = engine === 'mysql'
|
|
34
|
+
? isLocal
|
|
35
|
+
? 'Ensure MySQL is started in XAMPP Control Panel.'
|
|
36
|
+
: `Ensure the MySQL server at ${host}:${port} is running and reachable.`
|
|
37
|
+
: `Ensure the PostgreSQL service is running on ${host}:${port}.`;
|
|
38
|
+
if (fragmentedKeys?.length) {
|
|
39
|
+
const shown = fragmentedKeys.filter((k) => /HOST|DATABASE|_DB$|DB_NAME$/.test(k));
|
|
40
|
+
return `Detected fragmented ${label} config (${(shown.length ? shown : fragmentedKeys).join(', ')}). ${action}`;
|
|
41
|
+
}
|
|
42
|
+
return isLocal ? `${label} refused connections on ${host}:${port}. ${action}` : null;
|
|
43
|
+
}
|
|
44
|
+
export async function introspectEnv(targetDir, env) {
|
|
45
|
+
const conns = resolveConnections(targetDir);
|
|
46
|
+
const conn = conns[env];
|
|
47
|
+
if (!conn) {
|
|
48
|
+
const missingKey = conns.templateInfo?.missingKeys.find((k) => k.targetEnv === env);
|
|
49
|
+
if (missingKey) {
|
|
50
|
+
const engineLabel = missingKey.engine ? ` (${ENGINE_LABEL[missingKey.engine] ?? missingKey.engine})` : '';
|
|
51
|
+
const templateName = conns.templateInfo?.templateFile ?? '.env.example';
|
|
52
|
+
const suggestion = `Found "${missingKey.key}"${engineLabel} in ${templateName}, but it is not set in your .env`;
|
|
53
|
+
return {
|
|
54
|
+
status: disconnectedStatus(`No ${env} database configured. ${suggestion}`, missingKey.engine ?? 'postgresql', '', {
|
|
55
|
+
sourceKey: missingKey.key,
|
|
56
|
+
detectedFromExample: true,
|
|
57
|
+
exampleFile: templateName,
|
|
58
|
+
suggestion,
|
|
59
|
+
}),
|
|
60
|
+
tables: [],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const key = env === 'dev' ? 'DEV_DATABASE_URL (or DATABASE_URL)' : 'PROD_DATABASE_URL';
|
|
64
|
+
return { status: disconnectedStatus(`No ${env} database configured. Set ${key} in .env`), tables: [] };
|
|
65
|
+
}
|
|
66
|
+
const result = await introspectDatabase(conn.url);
|
|
67
|
+
result.status.sourceKey = conn.sourceKey;
|
|
68
|
+
result.status.detectedFromExample = conn.detectedFromExample;
|
|
69
|
+
result.status.exampleFile = conn.exampleFile;
|
|
70
|
+
if (conn.synthesized)
|
|
71
|
+
result.status.synthesized = true;
|
|
72
|
+
if (!result.status.connected) {
|
|
73
|
+
const fragmentedKeys = env === 'dev' && conn.synthesized ? conns.templateInfo.fragmentedConfig?.sourceKeys : undefined;
|
|
74
|
+
const diagnostic = localStackDiagnostic(conn.url, conn.engine, result.status, fragmentedKeys ?? (conn.synthesized ? [conn.sourceKey] : undefined));
|
|
75
|
+
if (diagnostic)
|
|
76
|
+
result.status.suggestion = diagnostic;
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
/** DB_HOST -> DB_*, PROD_DB_HOST -> PROD_DB_*, PGHOST -> PG*. */
|
|
81
|
+
export function keyFamily(key) {
|
|
82
|
+
const m = /^(.*_)[^_]*$/.exec(key);
|
|
83
|
+
return m ? `${m[1]}*` : `${key.slice(0, 2)}*`;
|
|
84
|
+
}
|
package/dist/commands/db.js
CHANGED
|
@@ -2,24 +2,13 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
|
-
import { resolveConnections } from '../db/env-parser.js';
|
|
6
|
-
import { disconnectedStatus, introspectDatabase } from '../db/introspector.js';
|
|
7
5
|
import { diffSchemas, loadContractTables } from '../db/diff.js';
|
|
8
6
|
import { DEFAULT_STUDIO_PORT, startStudioServer, writeContractSchema } from '../server/studio-server.js';
|
|
9
7
|
import { renderStudioHtml } from '../server/studio-ui.js';
|
|
10
8
|
import { refuseHeadless } from '../core/human-gate.js';
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* (text and --json) uses masked URLs and structural metadata only.
|
|
15
|
-
*/
|
|
16
|
-
const ENGINE_LABEL = {
|
|
17
|
-
postgresql: 'PostgreSQL',
|
|
18
|
-
mysql: 'MySQL',
|
|
19
|
-
sqlite: 'SQLite',
|
|
20
|
-
mongodb: 'MongoDB',
|
|
21
|
-
firestore: 'Firebase Firestore',
|
|
22
|
-
};
|
|
9
|
+
import { resolveConnections } from '../db/env-parser.js';
|
|
10
|
+
import { introspectEnv, keyFamily } from './db/db-introspect.js';
|
|
11
|
+
import { printStatusLine, printDiff, printInspectTables } from './db/db-formatter.js';
|
|
23
12
|
function resolveDir(targetDirArg) {
|
|
24
13
|
return path.resolve(targetDirArg || process.cwd());
|
|
25
14
|
}
|
|
@@ -42,165 +31,6 @@ function parseEnvOption(value, fallback) {
|
|
|
42
31
|
process.exitCode = 1;
|
|
43
32
|
return null;
|
|
44
33
|
}
|
|
45
|
-
async function introspectEnv(targetDir, env) {
|
|
46
|
-
const conns = resolveConnections(targetDir);
|
|
47
|
-
const conn = conns[env];
|
|
48
|
-
if (!conn) {
|
|
49
|
-
const missingKey = conns.templateInfo?.missingKeys.find((k) => k.targetEnv === env);
|
|
50
|
-
if (missingKey) {
|
|
51
|
-
const engineLabel = missingKey.engine ? ` (${ENGINE_LABEL[missingKey.engine] ?? missingKey.engine})` : '';
|
|
52
|
-
const templateName = conns.templateInfo?.templateFile ?? '.env.example';
|
|
53
|
-
const suggestion = `Found "${missingKey.key}"${engineLabel} in ${templateName}, but it is not set in your .env`;
|
|
54
|
-
return {
|
|
55
|
-
status: disconnectedStatus(`No ${env} database configured. ${suggestion}`, missingKey.engine ?? 'postgresql', '', {
|
|
56
|
-
sourceKey: missingKey.key,
|
|
57
|
-
detectedFromExample: true,
|
|
58
|
-
exampleFile: templateName,
|
|
59
|
-
suggestion,
|
|
60
|
-
}),
|
|
61
|
-
tables: [],
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
const key = env === 'dev' ? 'DEV_DATABASE_URL (or DATABASE_URL)' : 'PROD_DATABASE_URL';
|
|
65
|
-
return { status: disconnectedStatus(`No ${env} database configured. Set ${key} in .env`), tables: [] };
|
|
66
|
-
}
|
|
67
|
-
const result = await introspectDatabase(conn.url);
|
|
68
|
-
result.status.sourceKey = conn.sourceKey;
|
|
69
|
-
result.status.detectedFromExample = conn.detectedFromExample;
|
|
70
|
-
result.status.exampleFile = conn.exampleFile;
|
|
71
|
-
if (conn.synthesized)
|
|
72
|
-
result.status.synthesized = true;
|
|
73
|
-
if (!result.status.connected) {
|
|
74
|
-
const fragmentedKeys = env === 'dev' && conn.synthesized ? conns.templateInfo.fragmentedConfig?.sourceKeys : undefined;
|
|
75
|
-
const diagnostic = localStackDiagnostic(conn.url, conn.engine, result.status, fragmentedKeys ?? (conn.synthesized ? [conn.sourceKey] : undefined));
|
|
76
|
-
if (diagnostic)
|
|
77
|
-
result.status.suggestion = diagnostic;
|
|
78
|
-
}
|
|
79
|
-
return result;
|
|
80
|
-
}
|
|
81
|
-
const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]', '0.0.0.0']);
|
|
82
|
-
/**
|
|
83
|
-
* Actionable hint when a local MySQL/PostgreSQL server refuses the connection, e.g. MySQL not started in
|
|
84
|
-
* the XAMPP Control Panel. Only host/port are read from the URL; credentials are never touched.
|
|
85
|
-
*/
|
|
86
|
-
function localStackDiagnostic(url, engine, status, fragmentedKeys) {
|
|
87
|
-
if (engine !== 'mysql' && engine !== 'postgresql')
|
|
88
|
-
return null;
|
|
89
|
-
if (!/ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|connection refused/i.test(status.error ?? ''))
|
|
90
|
-
return null;
|
|
91
|
-
let host = '';
|
|
92
|
-
let port = engine === 'mysql' ? 3306 : 5432;
|
|
93
|
-
try {
|
|
94
|
-
const parsed = new URL(url);
|
|
95
|
-
host = parsed.hostname;
|
|
96
|
-
if (parsed.port)
|
|
97
|
-
port = Number(parsed.port);
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
const label = ENGINE_LABEL[engine];
|
|
103
|
-
const isLocal = LOCAL_HOSTS.has(host);
|
|
104
|
-
const action = engine === 'mysql'
|
|
105
|
-
? isLocal
|
|
106
|
-
? 'Ensure MySQL is started in XAMPP Control Panel.'
|
|
107
|
-
: `Ensure the MySQL server at ${host}:${port} is running and reachable.`
|
|
108
|
-
: `Ensure the PostgreSQL service is running on ${host}:${port}.`;
|
|
109
|
-
if (fragmentedKeys?.length) {
|
|
110
|
-
const shown = fragmentedKeys.filter((k) => /HOST|DATABASE|_DB$|DB_NAME$/.test(k));
|
|
111
|
-
return `Detected fragmented ${label} config (${(shown.length ? shown : fragmentedKeys).join(', ')}). ${action}`;
|
|
112
|
-
}
|
|
113
|
-
return isLocal ? `${label} refused connections on ${host}:${port}. ${action}` : null;
|
|
114
|
-
}
|
|
115
|
-
/** DB_HOST -> DB_*, PROD_DB_HOST -> PROD_DB_*, PGHOST -> PG*. */
|
|
116
|
-
function keyFamily(key) {
|
|
117
|
-
const m = /^(.*_)[^_]*$/.exec(key);
|
|
118
|
-
return m ? `${m[1]}*` : `${key.slice(0, 2)}*`;
|
|
119
|
-
}
|
|
120
|
-
function printStatusLine(env, s) {
|
|
121
|
-
const label = pc.bold(env.toUpperCase().padEnd(4));
|
|
122
|
-
const synthBadge = s.synthesized && s.sourceKey ? ` ${pc.magenta(`[synthesized from ${keyFamily(s.sourceKey)}]`)}` : '';
|
|
123
|
-
if (!s.connected) {
|
|
124
|
-
console.log(` ${pc.red('●')} ${label} ${pc.red('[OFFLINE]')}${synthBadge} ${pc.dim(s.error ?? 'Unknown error')}`);
|
|
125
|
-
if (s.suggestion) {
|
|
126
|
-
const isDiagnostic = /Ensure (MySQL|the MySQL|the PostgreSQL)/.test(s.suggestion);
|
|
127
|
-
console.log(` ${pc.yellow(isDiagnostic ? `[DIAGNOSTIC] ${s.suggestion}` : `Tip: ${s.suggestion}`)}`);
|
|
128
|
-
}
|
|
129
|
-
if (s.maskedUrl)
|
|
130
|
-
console.log(pc.dim(` ${s.maskedUrl}`));
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
const count = s.entityCount ?? s.tableCount;
|
|
134
|
-
const isCollection = s.entityType === 'collection';
|
|
135
|
-
const entityWord = isCollection ? (count === 1 ? 'collection' : 'collections') : (count === 1 ? 'table' : 'tables');
|
|
136
|
-
const sourceInfo = synthBadge
|
|
137
|
-
? synthBadge
|
|
138
|
-
: s.detectedFromExample && s.sourceKey
|
|
139
|
-
? pc.cyan(` (via ${s.sourceKey} from ${s.exampleFile ?? '.env.example'})`)
|
|
140
|
-
: s.sourceKey
|
|
141
|
-
? pc.dim(` (via ${s.sourceKey})`)
|
|
142
|
-
: '';
|
|
143
|
-
console.log(` ${pc.green('●')} ${label} ${pc.green('[ONLINE]')} ${ENGINE_LABEL[s.engine] ?? s.engine}` +
|
|
144
|
-
`${s.database ? ` · ${s.database}` : ''} ${pc.dim(`(${s.pingMs}ms)`)} – ${count} ${entityWord}${sourceInfo}`);
|
|
145
|
-
console.log(pc.dim(` ${s.maskedUrl}`));
|
|
146
|
-
}
|
|
147
|
-
function describeColumn(c) {
|
|
148
|
-
const flags = [];
|
|
149
|
-
if (c.primaryKey)
|
|
150
|
-
flags.push('PK');
|
|
151
|
-
if (c.isMedia)
|
|
152
|
-
flags.push('[IMAGE]');
|
|
153
|
-
if (c.isSubcollection)
|
|
154
|
-
flags.push('[SUBCOLLECTION]');
|
|
155
|
-
const flagStr = flags.length ? ` ${pc.magenta(flags.join(' '))}` : '';
|
|
156
|
-
return `${c.type}${c.nullable ? '' : ' NOT NULL'}${c.default != null ? ` DEFAULT ${c.default}` : ''}${flagStr}`;
|
|
157
|
-
}
|
|
158
|
-
function printDiff(diff, targetLabel) {
|
|
159
|
-
const s = diff.summary;
|
|
160
|
-
console.log(pc.bold(pc.cyan(`\nSchema Drift: Dev → ${targetLabel}`)));
|
|
161
|
-
console.log(` ${pc.green(`+${s.addedTablesCount} added`)} ${pc.yellow(`~${s.alteredTablesCount} altered`)} ` +
|
|
162
|
-
`${pc.red(`-${s.droppedTablesCount} dropped`)} ${pc.dim(`${s.unchangedTablesCount} unchanged`)}`);
|
|
163
|
-
if (!s.addedTablesCount && !s.alteredTablesCount && !s.droppedTablesCount) {
|
|
164
|
-
console.log(pc.green(`\n✔ In sync: Dev matches ${targetLabel}.\n`));
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
for (const t of diff.addedTables) {
|
|
168
|
-
const isCol = t.entityType === 'collection';
|
|
169
|
-
const tag = isCol ? '[NEW COLLECTION]' : '[NEW TABLE]';
|
|
170
|
-
const fieldWord = isCol ? 'fields' : 'columns';
|
|
171
|
-
console.log(`\n ${pc.green(tag)} ${pc.bold(t.name)} ${pc.dim(`(${t.columns.length} ${fieldWord})`)}`);
|
|
172
|
-
}
|
|
173
|
-
for (const t of diff.alteredTables) {
|
|
174
|
-
console.log(`\n ${pc.yellow('[ALTERED]')} ${pc.bold(t.name)}${t.isDestructive ? ` ${pc.red('[DESTRUCTIVE]')}` : ''}`);
|
|
175
|
-
for (const c of t.addedColumns)
|
|
176
|
-
console.log(pc.green(` + ${c.name}: ${describeColumn(c)}`));
|
|
177
|
-
for (const ch of t.alteredColumns) {
|
|
178
|
-
console.log(pc.yellow(` ~ ${ch.name} (${ch.changedFields.join(', ')}): ${describeColumn(ch.before)} → ${describeColumn(ch.after)}`));
|
|
179
|
-
}
|
|
180
|
-
for (const c of t.droppedColumns)
|
|
181
|
-
console.log(pc.red(` - ${c.name}: ${describeColumn(c)}`));
|
|
182
|
-
for (const i of t.addedIndexes)
|
|
183
|
-
console.log(pc.green(` + index ${i.name} (${i.columns.join(', ')})${i.unique ? ' unique' : ''}`));
|
|
184
|
-
for (const i of t.droppedIndexes)
|
|
185
|
-
console.log(pc.red(` - index ${i.name} (${i.columns.join(', ')})`));
|
|
186
|
-
for (const f of t.addedForeignKeys)
|
|
187
|
-
console.log(pc.green(` + FK ${f.column} → ${f.referencedTable}.${f.referencedColumn}`));
|
|
188
|
-
for (const f of t.droppedForeignKeys)
|
|
189
|
-
console.log(pc.red(` - FK ${f.column} → ${f.referencedTable}.${f.referencedColumn}`));
|
|
190
|
-
}
|
|
191
|
-
for (const t of diff.droppedTables) {
|
|
192
|
-
const isCol = t.entityType === 'collection';
|
|
193
|
-
const tag = isCol ? '[DROPPED COLLECTION]' : '[DROPPED]';
|
|
194
|
-
const fieldWord = isCol ? 'fields' : 'columns';
|
|
195
|
-
console.log(`\n ${pc.red(tag)} ${pc.bold(t.name)} ${pc.dim(`(${t.columns.length} ${fieldWord})`)}`);
|
|
196
|
-
}
|
|
197
|
-
if (s.hasDestructiveChanges) {
|
|
198
|
-
console.log(pc.bgRed(pc.white(pc.bold(' HIGH SEVERITY '))) +
|
|
199
|
-
pc.red(` Destructive changes detected against ${targetLabel}. Applying them would drop data or fail on existing rows.`));
|
|
200
|
-
}
|
|
201
|
-
console.log('');
|
|
202
|
-
}
|
|
203
|
-
// ─── nativ db status ─────────────────────────────────────────────────────────
|
|
204
34
|
export async function runDbStatus(targetDirArg, options = {}) {
|
|
205
35
|
const targetDir = resolveDir(targetDirArg);
|
|
206
36
|
const [dev, prod] = await Promise.all([introspectEnv(targetDir, 'dev'), introspectEnv(targetDir, 'prod')]);
|
|
@@ -215,7 +45,6 @@ export async function runDbStatus(targetDirArg, options = {}) {
|
|
|
215
45
|
if (!dev.status.connected && !prod.status.connected)
|
|
216
46
|
process.exitCode = 1;
|
|
217
47
|
}
|
|
218
|
-
// ─── nativ db inspect ────────────────────────────────────────────────────────
|
|
219
48
|
export async function runDbInspect(targetDirArg, options = {}) {
|
|
220
49
|
const env = parseEnvOption(options.env, 'dev');
|
|
221
50
|
if (!env)
|
|
@@ -247,37 +76,8 @@ export async function runDbInspect(targetDirArg, options = {}) {
|
|
|
247
76
|
console.log(JSON.stringify({ status: result.status, tables }, null, 2));
|
|
248
77
|
return;
|
|
249
78
|
}
|
|
250
|
-
|
|
251
|
-
for (const t of tables) {
|
|
252
|
-
const isCol = t.entityType === 'collection' || isCollection;
|
|
253
|
-
const fieldWord = isCol ? 'fields' : 'columns';
|
|
254
|
-
const tag = isCol ? pc.green('[COLLECTION] ') : '';
|
|
255
|
-
console.log(`\n ${tag}${pc.bold(t.name)} ${pc.dim(`(${t.columns.length} ${fieldWord})`)}`);
|
|
256
|
-
if (t.description)
|
|
257
|
-
console.log(pc.dim(` ${t.description}`));
|
|
258
|
-
const fkByColumn = new Map(t.foreignKeys.map((f) => [f.column.toLowerCase(), f]));
|
|
259
|
-
const nameWidth = Math.max(...t.columns.map((c) => c.name.length), 4);
|
|
260
|
-
for (const c of t.columns) {
|
|
261
|
-
const fk = fkByColumn.get(c.name.toLowerCase());
|
|
262
|
-
const flags = [];
|
|
263
|
-
if (c.primaryKey)
|
|
264
|
-
flags.push(pc.magenta('PK'));
|
|
265
|
-
if (c.isMedia)
|
|
266
|
-
flags.push(pc.magenta('[IMAGE]'));
|
|
267
|
-
if (c.isSubcollection)
|
|
268
|
-
flags.push(pc.cyan('[SUBCOLLECTION]'));
|
|
269
|
-
if (fk)
|
|
270
|
-
flags.push(pc.cyan(`FK→${fk.referencedTable}.${fk.referencedColumn}`));
|
|
271
|
-
const tags = flags.join(' ');
|
|
272
|
-
console.log(` ${c.name.padEnd(nameWidth)} ${c.type}${c.nullable ? '' : pc.dim(' NOT NULL')}` +
|
|
273
|
-
`${c.default != null ? pc.dim(` DEFAULT ${c.default}`) : ''}${tags ? ` ${tags}` : ''}`);
|
|
274
|
-
}
|
|
275
|
-
for (const i of t.indexes)
|
|
276
|
-
console.log(pc.dim(` index ${i.name} (${i.columns.join(', ')})${i.unique ? ' unique' : ''}`));
|
|
277
|
-
}
|
|
278
|
-
console.log('');
|
|
79
|
+
printInspectTables(env, result.status.maskedUrl, tables, entityWord, isCollection);
|
|
279
80
|
}
|
|
280
|
-
// ─── nativ db diff ───────────────────────────────────────────────────────────
|
|
281
81
|
export async function runDbDiff(targetDirArg, options = {}) {
|
|
282
82
|
const target = (options.target ?? 'prod').toLowerCase();
|
|
283
83
|
if (target !== 'prod' && target !== 'contract') {
|
|
@@ -318,7 +118,6 @@ export async function runDbDiff(targetDirArg, options = {}) {
|
|
|
318
118
|
if (options.exitCode && s.addedTablesCount + s.alteredTablesCount + s.droppedTablesCount > 0)
|
|
319
119
|
process.exitCode = 1;
|
|
320
120
|
}
|
|
321
|
-
// ─── nativ db sync ───────────────────────────────────────────────────────────
|
|
322
121
|
/** Exports a live schema into .ai/db_schema.json. Previews unless --yes is passed. */
|
|
323
122
|
export async function runDbSync(targetDirArg, options = {}) {
|
|
324
123
|
const source = parseEnvOption(options.source, 'dev');
|
|
@@ -352,7 +151,6 @@ export async function runDbSync(targetDirArg, options = {}) {
|
|
|
352
151
|
const entityWord = isCollection ? (count === 1 ? 'collection' : 'collections') : (count === 1 ? 'table' : 'tables');
|
|
353
152
|
console.log(pc.green(`✔ Wrote ${count} ${entityWord} (structure only, no credentials) to ${path.relative(targetDir, filePath)}\n`));
|
|
354
153
|
}
|
|
355
|
-
// ─── nativ db ui ─────────────────────────────────────────────────────────────
|
|
356
154
|
function openBrowser(url) {
|
|
357
155
|
const [cmd, args] = process.platform === 'win32'
|
|
358
156
|
? ['cmd', ['/c', 'start', '""', url]]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import { loadProfile, setProfile, PROFILE_DEFAULTS } from '../core/profiles.js';
|
|
4
|
+
export async function runProfileCommand(profileArg, targetDirArg, options = {}) {
|
|
5
|
+
const root = path.resolve(targetDirArg || process.cwd());
|
|
6
|
+
if (!profileArg) {
|
|
7
|
+
const current = loadProfile(root);
|
|
8
|
+
const details = PROFILE_DEFAULTS[current];
|
|
9
|
+
if (options.json) {
|
|
10
|
+
console.log(JSON.stringify(details, null, 2));
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
console.log(pc.bold(pc.cyan(`\nCurrent nativ Profile: `)) + pc.yellow(current.toUpperCase()));
|
|
14
|
+
console.log(pc.dim(` ${details.description}\n`));
|
|
15
|
+
console.log(pc.dim(' • Enforcement: ') + pc.white(details.enforcement));
|
|
16
|
+
console.log(pc.dim(' • Governor: ') + pc.white(details.governorMode));
|
|
17
|
+
console.log(pc.dim(' • Test Integrity: ') + pc.white(details.testIntegrity));
|
|
18
|
+
console.log(pc.dim(' • Headless CI/CD: ') + pc.white(details.allowHeadlessBypass ? 'enabled' : 'restricted'));
|
|
19
|
+
console.log(pc.dim('\nTo switch profile: ') + pc.white('nativ profile [prototype | solo | enterprise]\n'));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const p = profileArg.toLowerCase();
|
|
23
|
+
if (p !== 'prototype' && p !== 'solo' && p !== 'enterprise') {
|
|
24
|
+
console.error(pc.red(`\n✖ Invalid profile "${profileArg}". Choose prototype, solo, or enterprise.\n`));
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const updated = setProfile(root, p);
|
|
29
|
+
if (options.json) {
|
|
30
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.log(pc.green(`\n✔ Switched active profile to `) + pc.bold(pc.cyan(p.toUpperCase())));
|
|
34
|
+
console.log(pc.dim(` ${updated.description}\n`));
|
|
35
|
+
console.log(pc.dim(' • Enforcement: ') + pc.white(updated.enforcement));
|
|
36
|
+
console.log(pc.dim(' • Governor: ') + pc.white(updated.governorMode));
|
|
37
|
+
console.log(pc.dim(' • Test Integrity: ') + pc.white(updated.testIntegrity));
|
|
38
|
+
console.log('');
|
|
39
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { MasterPlan } from '../../scanner/types.js';
|
|
2
|
+
import { TaskAddOptions, TaskAddResult } from './task-common.js';
|
|
3
|
+
export declare const FAST_PATH_MILESTONE_NAME = "Fast-Path Tasks";
|
|
4
|
+
export declare function generateNextTaskId(plan: MasterPlan): string;
|
|
5
|
+
export declare function generateNextMilestoneId(plan: MasterPlan): string;
|
|
6
|
+
/**
|
|
7
|
+
* Appends a task to .ai/master_plan.json with an auto-incremented ID.
|
|
8
|
+
* Routing: --milestone wins; otherwise fast-path tasks go to the fast-path milestone (created on
|
|
9
|
+
* first use) and planned tasks to the active milestone. Completed milestones are reopened.
|
|
10
|
+
*/
|
|
11
|
+
export declare function runTaskAdd(title: string, targetDirArg?: string, options?: TaskAddOptions): Promise<TaskAddResult | null>;
|