astrocode-workflow 0.0.1
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/LICENSE +1 -0
- package/README.md +85 -0
- package/dist/agents/commands.d.ts +9 -0
- package/dist/agents/commands.js +121 -0
- package/dist/agents/prompts.d.ts +2 -0
- package/dist/agents/prompts.js +27 -0
- package/dist/agents/registry.d.ts +6 -0
- package/dist/agents/registry.js +223 -0
- package/dist/agents/types.d.ts +14 -0
- package/dist/agents/types.js +8 -0
- package/dist/config/config-handler.d.ts +4 -0
- package/dist/config/config-handler.js +46 -0
- package/dist/config/defaults.d.ts +3 -0
- package/dist/config/defaults.js +3 -0
- package/dist/config/loader.d.ts +11 -0
- package/dist/config/loader.js +48 -0
- package/dist/config/schema.d.ts +176 -0
- package/dist/config/schema.js +198 -0
- package/dist/hooks/continuation-enforcer.d.ts +26 -0
- package/dist/hooks/continuation-enforcer.js +166 -0
- package/dist/hooks/tool-output-truncator.d.ts +17 -0
- package/dist/hooks/tool-output-truncator.js +56 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +108 -0
- package/dist/shared/deep-merge.d.ts +8 -0
- package/dist/shared/deep-merge.js +25 -0
- package/dist/shared/hash.d.ts +1 -0
- package/dist/shared/hash.js +4 -0
- package/dist/shared/log.d.ts +7 -0
- package/dist/shared/log.js +24 -0
- package/dist/shared/model-tuning.d.ts +9 -0
- package/dist/shared/model-tuning.js +28 -0
- package/dist/shared/paths.d.ts +19 -0
- package/dist/shared/paths.js +51 -0
- package/dist/shared/text.d.ts +4 -0
- package/dist/shared/text.js +19 -0
- package/dist/shared/time.d.ts +1 -0
- package/dist/shared/time.js +3 -0
- package/dist/state/adapters/index.d.ts +39 -0
- package/dist/state/adapters/index.js +119 -0
- package/dist/state/db.d.ts +17 -0
- package/dist/state/db.js +83 -0
- package/dist/state/ids.d.ts +8 -0
- package/dist/state/ids.js +25 -0
- package/dist/state/schema.d.ts +2 -0
- package/dist/state/schema.js +247 -0
- package/dist/state/types.d.ts +70 -0
- package/dist/state/types.js +1 -0
- package/dist/tools/artifacts.d.ts +18 -0
- package/dist/tools/artifacts.js +71 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +100 -0
- package/dist/tools/init.d.ts +8 -0
- package/dist/tools/init.js +41 -0
- package/dist/tools/injects.d.ts +23 -0
- package/dist/tools/injects.js +99 -0
- package/dist/tools/repair.d.ts +8 -0
- package/dist/tools/repair.js +25 -0
- package/dist/tools/run.d.ts +13 -0
- package/dist/tools/run.js +54 -0
- package/dist/tools/spec.d.ts +13 -0
- package/dist/tools/spec.js +41 -0
- package/dist/tools/stage.d.ts +23 -0
- package/dist/tools/stage.js +284 -0
- package/dist/tools/status.d.ts +8 -0
- package/dist/tools/status.js +107 -0
- package/dist/tools/story.d.ts +23 -0
- package/dist/tools/story.js +85 -0
- package/dist/tools/workflow.d.ts +8 -0
- package/dist/tools/workflow.js +197 -0
- package/dist/ui/inject.d.ts +5 -0
- package/dist/ui/inject.js +9 -0
- package/dist/ui/toasts.d.ts +13 -0
- package/dist/ui/toasts.js +39 -0
- package/dist/workflow/artifacts.d.ts +24 -0
- package/dist/workflow/artifacts.js +45 -0
- package/dist/workflow/baton.d.ts +66 -0
- package/dist/workflow/baton.js +101 -0
- package/dist/workflow/context.d.ts +12 -0
- package/dist/workflow/context.js +67 -0
- package/dist/workflow/directives.d.ts +37 -0
- package/dist/workflow/directives.js +111 -0
- package/dist/workflow/repair.d.ts +8 -0
- package/dist/workflow/repair.js +99 -0
- package/dist/workflow/state-machine.d.ts +43 -0
- package/dist/workflow/state-machine.js +127 -0
- package/dist/workflow/story-helpers.d.ts +9 -0
- package/dist/workflow/story-helpers.js +13 -0
- package/package.json +32 -0
- package/src/agents/commands.ts +137 -0
- package/src/agents/prompts.ts +28 -0
- package/src/agents/registry.ts +310 -0
- package/src/agents/types.ts +31 -0
- package/src/config/config-handler.ts +48 -0
- package/src/config/defaults.ts +4 -0
- package/src/config/loader.ts +55 -0
- package/src/config/schema.ts +236 -0
- package/src/hooks/continuation-enforcer.ts +217 -0
- package/src/hooks/tool-output-truncator.ts +82 -0
- package/src/index.ts +131 -0
- package/src/shared/deep-merge.ts +28 -0
- package/src/shared/hash.ts +5 -0
- package/src/shared/log.ts +30 -0
- package/src/shared/model-tuning.ts +48 -0
- package/src/shared/paths.ts +70 -0
- package/src/shared/text.ts +20 -0
- package/src/shared/time.ts +3 -0
- package/src/shims.node.d.ts +20 -0
- package/src/state/adapters/index.ts +155 -0
- package/src/state/db.ts +105 -0
- package/src/state/ids.ts +33 -0
- package/src/state/schema.ts +249 -0
- package/src/state/types.ts +76 -0
- package/src/tools/artifacts.ts +83 -0
- package/src/tools/index.ts +111 -0
- package/src/tools/init.ts +50 -0
- package/src/tools/injects.ts +108 -0
- package/src/tools/repair.ts +31 -0
- package/src/tools/run.ts +62 -0
- package/src/tools/spec.ts +50 -0
- package/src/tools/stage.ts +361 -0
- package/src/tools/status.ts +119 -0
- package/src/tools/story.ts +106 -0
- package/src/tools/workflow.ts +241 -0
- package/src/ui/inject.ts +13 -0
- package/src/ui/toasts.ts +48 -0
- package/src/workflow/artifacts.ts +69 -0
- package/src/workflow/baton.ts +141 -0
- package/src/workflow/context.ts +86 -0
- package/src/workflow/directives.ts +170 -0
- package/src/workflow/repair.ts +138 -0
- package/src/workflow/state-machine.ts +194 -0
- package/src/workflow/story-helpers.ts +18 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
2
|
+
|
|
3
|
+
let CURRENT_LEVEL: LogLevel = (process.env.ASTRO_LOG_LEVEL as LogLevel) ?? "info";
|
|
4
|
+
|
|
5
|
+
const ORDER: Record<LogLevel, number> = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
6
|
+
|
|
7
|
+
export function setLogLevel(level: LogLevel) {
|
|
8
|
+
CURRENT_LEVEL = level;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function shouldLog(level: LogLevel): boolean {
|
|
12
|
+
return ORDER[level] >= ORDER[CURRENT_LEVEL];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function log(level: LogLevel, message: string, meta?: unknown) {
|
|
16
|
+
if (!shouldLog(level)) return;
|
|
17
|
+
const prefix = `[astrocode:${level}]`;
|
|
18
|
+
if (meta === undefined) {
|
|
19
|
+
// eslint-disable-next-line no-console
|
|
20
|
+
console.log(prefix, message);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// eslint-disable-next-line no-console
|
|
24
|
+
console.log(prefix, message, meta);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const debug = (msg: string, meta?: unknown) => log("debug", msg, meta);
|
|
28
|
+
export const info = (msg: string, meta?: unknown) => log("info", msg, meta);
|
|
29
|
+
export const warn = (msg: string, meta?: unknown) => log("warn", msg, meta);
|
|
30
|
+
export const error = (msg: string, meta?: unknown) => log("error", msg, meta);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { AgentConfig } from "@opencode-ai/sdk";
|
|
2
|
+
import { isGptModel } from "../agents/types";
|
|
3
|
+
|
|
4
|
+
export type AstroCognitionPreset =
|
|
5
|
+
| "orchestrator"
|
|
6
|
+
| "frame"
|
|
7
|
+
| "plan"
|
|
8
|
+
| "spec"
|
|
9
|
+
| "implement"
|
|
10
|
+
| "review"
|
|
11
|
+
| "verify"
|
|
12
|
+
| "close"
|
|
13
|
+
| "utility";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* OMO-style: if GPT model => reasoningEffort/textVerbosity.
|
|
17
|
+
* Otherwise => Anthropic-style thinking budget.
|
|
18
|
+
*
|
|
19
|
+
* Not all providers honor these fields; safe to include.
|
|
20
|
+
*/
|
|
21
|
+
export function applyModelTuning(base: AgentConfig, preset: AstroCognitionPreset): AgentConfig {
|
|
22
|
+
const model = base.model ?? "";
|
|
23
|
+
if (!model) return base;
|
|
24
|
+
|
|
25
|
+
if (isGptModel(model)) {
|
|
26
|
+
const reasoningEffort =
|
|
27
|
+
preset === "orchestrator" ? "medium"
|
|
28
|
+
: preset === "implement" ? "high"
|
|
29
|
+
: "medium";
|
|
30
|
+
|
|
31
|
+
const textVerbosity =
|
|
32
|
+
preset === "orchestrator" ? "low"
|
|
33
|
+
: preset === "review" ? "high"
|
|
34
|
+
: "medium";
|
|
35
|
+
|
|
36
|
+
const { thinking, ...rest } = base as any;
|
|
37
|
+
return { ...rest, reasoningEffort, textVerbosity } as AgentConfig;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const budgetTokens =
|
|
41
|
+
preset === "implement" ? 32_000
|
|
42
|
+
: preset === "review" ? 24_000
|
|
43
|
+
: preset === "orchestrator" ? 16_000
|
|
44
|
+
: 12_000;
|
|
45
|
+
|
|
46
|
+
const { reasoningEffort, textVerbosity, ...rest } = base as any;
|
|
47
|
+
return { ...rest, thinking: { type: "enabled", budgetTokens } } as AgentConfig;
|
|
48
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
|
|
4
|
+
/** Normalize to posix-like separators for DB paths. */
|
|
5
|
+
export function toPosix(p: string): string {
|
|
6
|
+
return p.split(path.sep).join("/");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function ensureDir(p: string) {
|
|
10
|
+
fs.mkdirSync(p, { recursive: true });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function joinRepo(root: string, ...parts: string[]): string {
|
|
14
|
+
return path.join(root, ...parts);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type AstroPaths = {
|
|
18
|
+
repoRoot: string;
|
|
19
|
+
astroRoot: string;
|
|
20
|
+
dbPath: string;
|
|
21
|
+
runsDir: string;
|
|
22
|
+
specPath: string;
|
|
23
|
+
toolOutputDir: string;
|
|
24
|
+
configPathPreferred: string;
|
|
25
|
+
configPathFallback: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function getAstroPaths(repoRoot: string, dbPathOverride?: string): AstroPaths {
|
|
29
|
+
const astroRoot = joinRepo(repoRoot, ".astro");
|
|
30
|
+
const runsDir = joinRepo(repoRoot, ".astro", "runs");
|
|
31
|
+
const dbPath = dbPathOverride ? joinRepo(repoRoot, dbPathOverride) : joinRepo(repoRoot, ".astro", "astro.db");
|
|
32
|
+
const specPath = joinRepo(repoRoot, ".astro", "spec.md");
|
|
33
|
+
const toolOutputDir = joinRepo(repoRoot, ".astro", "tool_output");
|
|
34
|
+
return {
|
|
35
|
+
repoRoot,
|
|
36
|
+
astroRoot,
|
|
37
|
+
dbPath,
|
|
38
|
+
runsDir,
|
|
39
|
+
specPath,
|
|
40
|
+
toolOutputDir,
|
|
41
|
+
configPathPreferred: joinRepo(repoRoot, ".astro", "astrocode.config.jsonc"),
|
|
42
|
+
configPathFallback: joinRepo(repoRoot, "astrocode.config.jsonc"),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function ensureAstroDirs(paths: AstroPaths) {
|
|
47
|
+
ensureDir(paths.astroRoot);
|
|
48
|
+
ensureDir(paths.runsDir);
|
|
49
|
+
ensureDir(paths.toolOutputDir);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function runDir(paths: AstroPaths, runId: string): string {
|
|
53
|
+
return joinRepo(paths.repoRoot, ".astro", "runs", runId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function stageDir(paths: AstroPaths, runId: string, stageKey: string): string {
|
|
57
|
+
return joinRepo(paths.repoRoot, ".astro", "runs", runId, stageKey);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function assertInsideAstro(repoRoot: string, filePath: string) {
|
|
61
|
+
const absRepo = path.resolve(repoRoot);
|
|
62
|
+
const abs = path.resolve(filePath);
|
|
63
|
+
const astroRoot = path.resolve(path.join(repoRoot, ".astro"));
|
|
64
|
+
if (!abs.startsWith(astroRoot + path.sep) && abs !== astroRoot) {
|
|
65
|
+
throw new Error(`Refusing to write outside .astro: ${filePath}`);
|
|
66
|
+
}
|
|
67
|
+
if (!abs.startsWith(absRepo + path.sep) && abs !== absRepo) {
|
|
68
|
+
throw new Error(`Refusing to write outside repo root: ${filePath}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function normalizeNewlines(s: string): string {
|
|
2
|
+
return s.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function clampLines(md: string, maxLines: number): string {
|
|
6
|
+
const lines = normalizeNewlines(md).split("\n");
|
|
7
|
+
if (lines.length <= maxLines) return md.trimEnd();
|
|
8
|
+
return lines.slice(0, maxLines).join("\n").trimEnd() + "\n…";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function clampChars(s: string, maxChars: number): string {
|
|
12
|
+
if (s.length <= maxChars) return s;
|
|
13
|
+
return s.slice(0, maxChars) + "\n…(truncated)";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function stripCodeFences(md: string): string {
|
|
17
|
+
// Light helper: remove surrounding triple backticks if present
|
|
18
|
+
const m = md.match(/^```[a-zA-Z0-9_-]*\n([\s\S]*)\n```\s*$/);
|
|
19
|
+
return m ? m[1] : md;
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Minimal Node.js type shims so this package can compile in environments
|
|
3
|
+
where @types/node is not installed (e.g., offline installs).
|
|
4
|
+
|
|
5
|
+
Keep this file tiny on purpose: only add declarations that are actually
|
|
6
|
+
referenced in the codebase.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
declare var require: any;
|
|
10
|
+
|
|
11
|
+
declare var process: {
|
|
12
|
+
env: Record<string, string | undefined>;
|
|
13
|
+
cwd?: () => string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
declare var __dirname: string;
|
|
17
|
+
|
|
18
|
+
declare class Buffer {
|
|
19
|
+
static from(input: any, encoding?: string): Buffer;
|
|
20
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { info, warn } from "../../shared/log";
|
|
2
|
+
|
|
3
|
+
export interface DatabaseConnection {
|
|
4
|
+
pragma(sql: string): void;
|
|
5
|
+
exec(sql: string): void;
|
|
6
|
+
prepare(sql: string): Statement;
|
|
7
|
+
close(): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface Statement {
|
|
11
|
+
run(...params: any[]): { changes: number; lastInsertRowid: any };
|
|
12
|
+
get(...params: any[]): any;
|
|
13
|
+
all(...params: any[]): any[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DatabaseAdapter {
|
|
17
|
+
isAvailable(): boolean;
|
|
18
|
+
open(path: string, opts?: { busyTimeoutMs?: number }): DatabaseConnection;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Mock adapter for when no database is available
|
|
22
|
+
export class MockDatabaseAdapter implements DatabaseAdapter {
|
|
23
|
+
isAvailable(): boolean {
|
|
24
|
+
return true; // Mock is always available
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
open(): DatabaseConnection {
|
|
28
|
+
warn("Using mock database - no persistence available");
|
|
29
|
+
return {
|
|
30
|
+
pragma: () => {},
|
|
31
|
+
exec: () => {},
|
|
32
|
+
prepare: () => ({
|
|
33
|
+
run: () => ({ changes: 0, lastInsertRowid: null }),
|
|
34
|
+
get: () => null,
|
|
35
|
+
all: () => []
|
|
36
|
+
}),
|
|
37
|
+
close: () => {}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Bun SQLite adapter - singleton pattern to avoid multiple initializations
|
|
43
|
+
let bunDatabase: any = null;
|
|
44
|
+
let bunDatabaseInitialized = false;
|
|
45
|
+
|
|
46
|
+
function initializeBunDatabase(): any {
|
|
47
|
+
if (bunDatabaseInitialized) {
|
|
48
|
+
return bunDatabase;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
bunDatabaseInitialized = true;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const isBun = typeof (globalThis as any).Bun !== 'undefined';
|
|
55
|
+
if (isBun) {
|
|
56
|
+
const { Database } = require('bun:sqlite');
|
|
57
|
+
bunDatabase = Database;
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// bun:sqlite not available
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return bunDatabase;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Bun SQLite adapter
|
|
67
|
+
export class BunSqliteAdapter implements DatabaseAdapter {
|
|
68
|
+
isAvailable(): boolean {
|
|
69
|
+
return !!initializeBunDatabase();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
open(path: string, opts?: { busyTimeoutMs?: number }): DatabaseConnection {
|
|
73
|
+
const Database = initializeBunDatabase();
|
|
74
|
+
if (!Database) {
|
|
75
|
+
throw new Error("bun:sqlite not available");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const db = new Database(path);
|
|
79
|
+
|
|
80
|
+
// Configure database
|
|
81
|
+
if (opts?.busyTimeoutMs) {
|
|
82
|
+
db.exec(`PRAGMA busy_timeout = ${opts.busyTimeoutMs}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
pragma: (sql: string) => db.exec(`PRAGMA ${sql}`),
|
|
87
|
+
exec: (sql: string) => db.exec(sql),
|
|
88
|
+
prepare: (sql: string) => {
|
|
89
|
+
const stmt = db.prepare(sql);
|
|
90
|
+
return {
|
|
91
|
+
run: (...params: any[]) => stmt.run(...params),
|
|
92
|
+
get: (...params: any[]) => stmt.get(...params),
|
|
93
|
+
all: (...params: any[]) => stmt.all(...params)
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
close: () => db.close()
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Better SQLite3 adapter
|
|
102
|
+
export class BetterSqliteAdapter implements DatabaseAdapter {
|
|
103
|
+
private Database: any = null;
|
|
104
|
+
|
|
105
|
+
constructor() {
|
|
106
|
+
try {
|
|
107
|
+
this.Database = require("better-sqlite3");
|
|
108
|
+
} catch (e) {
|
|
109
|
+
// better-sqlite3 not available
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
isAvailable(): boolean {
|
|
114
|
+
return !!this.Database;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
open(path: string, opts?: { busyTimeoutMs?: number }): DatabaseConnection {
|
|
118
|
+
if (!this.Database) {
|
|
119
|
+
throw new Error("better-sqlite3 not available");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const db = new this.Database(path);
|
|
123
|
+
|
|
124
|
+
// Set busy timeout if specified
|
|
125
|
+
if (opts?.busyTimeoutMs) {
|
|
126
|
+
db.pragma(`busy_timeout = ${opts.busyTimeoutMs}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
pragma: (sql: string) => db.pragma(sql.replace('PRAGMA ', '')),
|
|
131
|
+
exec: (sql: string) => db.exec(sql),
|
|
132
|
+
prepare: (sql: string) => db.prepare(sql),
|
|
133
|
+
close: () => db.close()
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Factory function to create the appropriate adapter
|
|
139
|
+
export function createDatabaseAdapter(): DatabaseAdapter {
|
|
140
|
+
const isBun = typeof (globalThis as any).Bun !== 'undefined';
|
|
141
|
+
|
|
142
|
+
if (isBun) {
|
|
143
|
+
const adapter = new BunSqliteAdapter();
|
|
144
|
+
if (adapter.isAvailable()) {
|
|
145
|
+
return adapter;
|
|
146
|
+
}
|
|
147
|
+
throw new Error("Bun runtime detected but bun:sqlite not available");
|
|
148
|
+
} else {
|
|
149
|
+
const adapter = new BetterSqliteAdapter();
|
|
150
|
+
if (adapter.isAvailable()) {
|
|
151
|
+
return adapter;
|
|
152
|
+
}
|
|
153
|
+
throw new Error("Node.js runtime detected but better-sqlite3 not available");
|
|
154
|
+
}
|
|
155
|
+
}
|
package/src/state/db.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { SCHEMA_SQL, SCHEMA_VERSION } from "./schema";
|
|
4
|
+
import { nowISO } from "../shared/time";
|
|
5
|
+
import { info, warn } from "../shared/log";
|
|
6
|
+
import { createDatabaseAdapter, DatabaseConnection } from "./adapters";
|
|
7
|
+
|
|
8
|
+
export type SqliteDb = DatabaseConnection;
|
|
9
|
+
|
|
10
|
+
/** Ensure directory exists for a file path. */
|
|
11
|
+
function ensureParentDir(filePath: string) {
|
|
12
|
+
const dir = path.dirname(filePath);
|
|
13
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function openSqlite(dbPath: string, opts?: { busyTimeoutMs?: number }): SqliteDb {
|
|
17
|
+
ensureParentDir(dbPath);
|
|
18
|
+
const adapter = createDatabaseAdapter();
|
|
19
|
+
const db = adapter.open(dbPath, opts);
|
|
20
|
+
return db;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function configurePragmas(
|
|
24
|
+
db: SqliteDb,
|
|
25
|
+
pragmas: {
|
|
26
|
+
journal_mode?: "WAL" | "DELETE";
|
|
27
|
+
synchronous?: "NORMAL" | "FULL" | "OFF";
|
|
28
|
+
foreign_keys?: boolean;
|
|
29
|
+
temp_store?: "DEFAULT" | "MEMORY" | "FILE";
|
|
30
|
+
}
|
|
31
|
+
) {
|
|
32
|
+
if (pragmas.journal_mode) db.pragma(`journal_mode = ${pragmas.journal_mode}`);
|
|
33
|
+
if (pragmas.synchronous) db.pragma(`synchronous = ${pragmas.synchronous}`);
|
|
34
|
+
if (typeof pragmas.foreign_keys === "boolean") db.pragma(`foreign_keys = ${pragmas.foreign_keys ? "ON" : "OFF"}`);
|
|
35
|
+
if (pragmas.temp_store) db.pragma(`temp_store = ${pragmas.temp_store}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** BEGIN IMMEDIATE transaction helper. */
|
|
39
|
+
export function withTx<T>(db: SqliteDb, fn: () => T): T {
|
|
40
|
+
const adapter = createDatabaseAdapter();
|
|
41
|
+
if (!adapter.isAvailable()) {
|
|
42
|
+
// No database available, just run the function
|
|
43
|
+
return fn();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
db.exec("BEGIN IMMEDIATE");
|
|
47
|
+
try {
|
|
48
|
+
const out = fn();
|
|
49
|
+
db.exec("COMMIT");
|
|
50
|
+
return out;
|
|
51
|
+
} catch (e) {
|
|
52
|
+
try {
|
|
53
|
+
db.exec("ROLLBACK");
|
|
54
|
+
} catch {
|
|
55
|
+
// ignore
|
|
56
|
+
}
|
|
57
|
+
throw e;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function ensureSchema(db: SqliteDb, opts?: { allowAutoMigrate?: boolean; failOnDowngrade?: boolean }) {
|
|
62
|
+
const adapter = createDatabaseAdapter();
|
|
63
|
+
if (!adapter.isAvailable()) {
|
|
64
|
+
// Silent skip for mock adapter
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
db.exec(SCHEMA_SQL);
|
|
70
|
+
|
|
71
|
+
const row = db.prepare("SELECT schema_version FROM repo_state WHERE id = 1").get() as { schema_version?: number } | undefined;
|
|
72
|
+
|
|
73
|
+
if (!row) {
|
|
74
|
+
const now = nowISO();
|
|
75
|
+
db.prepare(
|
|
76
|
+
"INSERT INTO repo_state (id, schema_version, created_at, updated_at) VALUES (1, ?, ?, ?)"
|
|
77
|
+
).run(SCHEMA_VERSION, now, now);
|
|
78
|
+
// Initialize story key seq
|
|
79
|
+
db.prepare("INSERT OR IGNORE INTO story_keyseq (id, next_story_num) VALUES (1, 1)").run();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const currentVersion = row.schema_version ?? 0;
|
|
84
|
+
|
|
85
|
+
if (currentVersion === SCHEMA_VERSION) return;
|
|
86
|
+
|
|
87
|
+
if (currentVersion > SCHEMA_VERSION && (opts?.failOnDowngrade ?? true)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Astrocode DB schema_version ${currentVersion} is newer than this plugin (${SCHEMA_VERSION}). Refusing to downgrade.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (currentVersion < SCHEMA_VERSION) {
|
|
94
|
+
if (!(opts?.allowAutoMigrate ?? true)) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Astrocode DB schema_version ${currentVersion} is older than required (${SCHEMA_VERSION}). Auto-migrate disabled.`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
// Additive schema: SCHEMA_SQL already created new tables/indexes if missing.
|
|
100
|
+
db.prepare("UPDATE repo_state SET schema_version = ?, updated_at = ? WHERE id = 1").run(SCHEMA_VERSION, nowISO());
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
// Schema operations might fail on mock adapter, silently ignore
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/state/ids.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export function newId(prefix: string): string {
|
|
4
|
+
return `${prefix}_${randomUUID()}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function newRunId(): string {
|
|
8
|
+
return newId("run");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function newStageRunId(): string {
|
|
12
|
+
return newId("stage");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function newArtifactId(): string {
|
|
16
|
+
return newId("art");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function newToolRunId(): string {
|
|
20
|
+
return newId("tool");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function newEventId(): string {
|
|
24
|
+
return newId("evt");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function newSnapshotId(): string {
|
|
28
|
+
return newId("snap");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function newBatchId(): string {
|
|
32
|
+
return newId("batch");
|
|
33
|
+
}
|