devworkspace 0.0.0-stage → 0.1.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/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/README.md +128 -3
- package/dist/agents/claude/adapter.d.ts +15 -0
- package/dist/agents/claude/adapter.js +107 -0
- package/dist/agents/claude/adapter.js.map +1 -0
- package/dist/agents/claude/content.d.ts +7 -0
- package/dist/agents/claude/content.js +136 -0
- package/dist/agents/claude/content.js.map +1 -0
- package/dist/agents/types.d.ts +26 -0
- package/dist/agents/types.js +2 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/cli/args.d.ts +197 -0
- package/dist/cli/args.js +94 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/help.d.ts +10 -0
- package/dist/cli/help.js +181 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/main.d.ts +2 -0
- package/dist/cli/main.js +19 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/render.d.ts +41 -0
- package/dist/cli/render.js +220 -0
- package/dist/cli/render.js.map +1 -0
- package/dist/cli/run.d.ts +10 -0
- package/dist/cli/run.js +339 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/config/schemas.d.ts +76 -0
- package/dist/config/schemas.js +83 -0
- package/dist/config/schemas.js.map +1 -0
- package/dist/config/validate.d.ts +22 -0
- package/dist/config/validate.js +76 -0
- package/dist/config/validate.js.map +1 -0
- package/dist/core/migrations.d.ts +15 -0
- package/dist/core/migrations.js +34 -0
- package/dist/core/migrations.js.map +1 -0
- package/dist/core/version.d.ts +2 -0
- package/dist/core/version.js +12 -0
- package/dist/core/version.js.map +1 -0
- package/dist/core/versioned.d.ts +13 -0
- package/dist/core/versioned.js +16 -0
- package/dist/core/versioned.js.map +1 -0
- package/dist/core/workspace.d.ts +34 -0
- package/dist/core/workspace.js +92 -0
- package/dist/core/workspace.js.map +1 -0
- package/dist/decisions/decision.d.ts +31 -0
- package/dist/decisions/decision.js +123 -0
- package/dist/decisions/decision.js.map +1 -0
- package/dist/discovery/commands.d.ts +12 -0
- package/dist/discovery/commands.js +92 -0
- package/dist/discovery/commands.js.map +1 -0
- package/dist/discovery/detect.d.ts +72 -0
- package/dist/discovery/detect.js +421 -0
- package/dist/discovery/detect.js.map +1 -0
- package/dist/discovery/ignore.d.ts +6 -0
- package/dist/discovery/ignore.js +76 -0
- package/dist/discovery/ignore.js.map +1 -0
- package/dist/discovery/scan.d.ts +24 -0
- package/dist/discovery/scan.js +132 -0
- package/dist/discovery/scan.js.map +1 -0
- package/dist/errors/index.d.ts +25 -0
- package/dist/errors/index.js +39 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/git/git.d.ts +27 -0
- package/dist/git/git.js +97 -0
- package/dist/git/git.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/agent-skills.d.ts +10 -0
- package/dist/integrations/agent-skills.js +45 -0
- package/dist/integrations/agent-skills.js.map +1 -0
- package/dist/integrations/graphify.d.ts +7 -0
- package/dist/integrations/graphify.js +54 -0
- package/dist/integrations/graphify.js.map +1 -0
- package/dist/integrations/index.d.ts +6 -0
- package/dist/integrations/index.js +30 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/integrations/omniroute.d.ts +7 -0
- package/dist/integrations/omniroute.js +24 -0
- package/dist/integrations/omniroute.js.map +1 -0
- package/dist/integrations/ponytail.d.ts +10 -0
- package/dist/integrations/ponytail.js +58 -0
- package/dist/integrations/ponytail.js.map +1 -0
- package/dist/integrations/probe.d.ts +6 -0
- package/dist/integrations/probe.js +41 -0
- package/dist/integrations/probe.js.map +1 -0
- package/dist/integrations/types.d.ts +25 -0
- package/dist/integrations/types.js +2 -0
- package/dist/integrations/types.js.map +1 -0
- package/dist/knowledge/facts.d.ts +43 -0
- package/dist/knowledge/facts.js +195 -0
- package/dist/knowledge/facts.js.map +1 -0
- package/dist/portability/bundle.d.ts +22 -0
- package/dist/portability/bundle.js +102 -0
- package/dist/portability/bundle.js.map +1 -0
- package/dist/prompts/onboarding.d.ts +19 -0
- package/dist/prompts/onboarding.js +53 -0
- package/dist/prompts/onboarding.js.map +1 -0
- package/dist/prompts/refresh.d.ts +15 -0
- package/dist/prompts/refresh.js +33 -0
- package/dist/prompts/refresh.js.map +1 -0
- package/dist/prompts/requirements.d.ts +9 -0
- package/dist/prompts/requirements.js +34 -0
- package/dist/prompts/requirements.js.map +1 -0
- package/dist/security/files.d.ts +5 -0
- package/dist/security/files.js +44 -0
- package/dist/security/files.js.map +1 -0
- package/dist/security/redact.d.ts +13 -0
- package/dist/security/redact.js +43 -0
- package/dist/security/redact.js.map +1 -0
- package/dist/services/agent.d.ts +11 -0
- package/dist/services/agent.js +42 -0
- package/dist/services/agent.js.map +1 -0
- package/dist/services/analysis.d.ts +45 -0
- package/dist/services/analysis.js +151 -0
- package/dist/services/analysis.js.map +1 -0
- package/dist/services/common.d.ts +18 -0
- package/dist/services/common.js +18 -0
- package/dist/services/common.js.map +1 -0
- package/dist/services/doctor.d.ts +27 -0
- package/dist/services/doctor.js +223 -0
- package/dist/services/doctor.js.map +1 -0
- package/dist/services/init.d.ts +18 -0
- package/dist/services/init.js +135 -0
- package/dist/services/init.js.map +1 -0
- package/dist/services/knowledge.d.ts +40 -0
- package/dist/services/knowledge.js +72 -0
- package/dist/services/knowledge.js.map +1 -0
- package/dist/services/migrate.d.ts +16 -0
- package/dist/services/migrate.js +54 -0
- package/dist/services/migrate.js.map +1 -0
- package/dist/services/portability.d.ts +27 -0
- package/dist/services/portability.js +155 -0
- package/dist/services/portability.js.map +1 -0
- package/dist/services/records.d.ts +8 -0
- package/dist/services/records.js +26 -0
- package/dist/services/records.js.map +1 -0
- package/dist/services/refresh.d.ts +22 -0
- package/dist/services/refresh.js +64 -0
- package/dist/services/refresh.js.map +1 -0
- package/dist/services/reset.d.ts +23 -0
- package/dist/services/reset.js +96 -0
- package/dist/services/reset.js.map +1 -0
- package/dist/services/status.d.ts +57 -0
- package/dist/services/status.js +111 -0
- package/dist/services/status.js.map +1 -0
- package/dist/services/tasks.d.ts +44 -0
- package/dist/services/tasks.js +136 -0
- package/dist/services/tasks.js.map +1 -0
- package/dist/sessions/session.d.ts +19 -0
- package/dist/sessions/session.js +53 -0
- package/dist/sessions/session.js.map +1 -0
- package/dist/tasks/routing.d.ts +27 -0
- package/dist/tasks/routing.js +206 -0
- package/dist/tasks/routing.js.map +1 -0
- package/dist/tasks/task.d.ts +90 -0
- package/dist/tasks/task.js +275 -0
- package/dist/tasks/task.js.map +1 -0
- package/dist/utils/cleanup.d.ts +3 -0
- package/dist/utils/cleanup.js +18 -0
- package/dist/utils/cleanup.js.map +1 -0
- package/dist/utils/fs.d.ts +17 -0
- package/dist/utils/fs.js +164 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/lock.d.ts +6 -0
- package/dist/utils/lock.js +77 -0
- package/dist/utils/lock.js.map +1 -0
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.js +18 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/paths.d.ts +18 -0
- package/dist/utils/paths.js +95 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/proc.d.ts +22 -0
- package/dist/utils/proc.js +74 -0
- package/dist/utils/proc.js.map +1 -0
- package/docs/architecture.md +53 -0
- package/docs/benchmarking.md +33 -0
- package/docs/claude.md +46 -0
- package/docs/contributing.md +64 -0
- package/docs/getting-started.md +79 -0
- package/docs/integrations.md +51 -0
- package/docs/knowledge.md +66 -0
- package/docs/portability.md +40 -0
- package/docs/routing.md +51 -0
- package/docs/security.md +44 -0
- package/docs/testing.md +58 -0
- package/docs/troubleshooting.md +27 -0
- package/package.json +68 -4
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DevWorkspaceError } from '../errors/index.js';
|
|
4
|
+
import { atomicWriteJson, ensureDir, pathExists, readJsonFile } from '../utils/fs.js';
|
|
5
|
+
import { validateProject, SCHEMA_VERSION } from '../config/schemas.js';
|
|
6
|
+
import { MIGRATIONS, migrateData } from './migrations.js';
|
|
7
|
+
export const WORKSPACE_DIR = '.devworkspace';
|
|
8
|
+
export function wsPaths(root) {
|
|
9
|
+
const dir = path.join(root, WORKSPACE_DIR);
|
|
10
|
+
const j = (...p) => path.join(dir, ...p);
|
|
11
|
+
return {
|
|
12
|
+
root,
|
|
13
|
+
dir,
|
|
14
|
+
projectJson: j('project.json'),
|
|
15
|
+
contextDir: j('context'),
|
|
16
|
+
facts: j('context', 'facts.json'),
|
|
17
|
+
commands: j('context', 'commands.json'),
|
|
18
|
+
decisionsDir: j('decisions'),
|
|
19
|
+
tasksActive: j('tasks', 'active'),
|
|
20
|
+
tasksCompleted: j('tasks', 'completed'),
|
|
21
|
+
sessionsDir: j('sessions'),
|
|
22
|
+
analysisDir: j('analysis'),
|
|
23
|
+
discovery: j('analysis', 'discovery.json'),
|
|
24
|
+
onboardingPrompt: j('analysis', 'onboarding-prompt.md'),
|
|
25
|
+
refreshPrompt: j('analysis', 'refresh-prompt.md'),
|
|
26
|
+
stateDir: j('state'),
|
|
27
|
+
analysisState: j('state', 'analysis.json'),
|
|
28
|
+
localDir: j('local'),
|
|
29
|
+
integrationsState: j('local', 'integrations.json'),
|
|
30
|
+
backupsDir: j('local', 'backups'),
|
|
31
|
+
gitignore: j('.gitignore'),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/** Walk upward from start looking for an initialised workspace. The workspace location is never stored in it. */
|
|
35
|
+
export async function findWorkspaceRoot(start) {
|
|
36
|
+
let dir = path.resolve(start);
|
|
37
|
+
for (;;) {
|
|
38
|
+
if (await pathExists(path.join(dir, WORKSPACE_DIR, 'project.json')))
|
|
39
|
+
return dir;
|
|
40
|
+
const parent = path.dirname(dir);
|
|
41
|
+
if (parent === dir)
|
|
42
|
+
return null;
|
|
43
|
+
dir = parent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function requireWorkspace(cwd) {
|
|
47
|
+
const root = await findWorkspaceRoot(cwd);
|
|
48
|
+
if (!root) {
|
|
49
|
+
throw new DevWorkspaceError('NOT_INITIALIZED', 'No DevWorkspace found in this directory or any parent.', {
|
|
50
|
+
hint: 'Run "devworkspace init" in the project root.',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return wsPaths(root);
|
|
54
|
+
}
|
|
55
|
+
export async function loadProject(ws, migrations = MIGRATIONS.project ?? []) {
|
|
56
|
+
const raw = await readJsonFile(ws.projectJson);
|
|
57
|
+
const { data } = migrateData(raw, SCHEMA_VERSION, migrations, 'project.json');
|
|
58
|
+
return validateProject(data);
|
|
59
|
+
}
|
|
60
|
+
export async function saveProject(ws, cfg) {
|
|
61
|
+
validateProject(cfg);
|
|
62
|
+
await backupIfVersionChanges(ws, ws.projectJson, cfg.schemaVersion);
|
|
63
|
+
await atomicWriteJson(ws.projectJson, cfg);
|
|
64
|
+
}
|
|
65
|
+
/** Before a write that upgrades a file's schemaVersion on disk, keep a copy of the old file. */
|
|
66
|
+
export async function backupIfVersionChanges(ws, file, newVersion, stamp) {
|
|
67
|
+
if (!(await pathExists(file)))
|
|
68
|
+
return null;
|
|
69
|
+
let onDisk;
|
|
70
|
+
try {
|
|
71
|
+
onDisk = (await readJsonFile(file)).schemaVersion;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
onDisk = undefined;
|
|
75
|
+
}
|
|
76
|
+
if (onDisk === newVersion)
|
|
77
|
+
return null;
|
|
78
|
+
const folder = stamp ?? new Date().toISOString().replace(/[:.]/g, '-');
|
|
79
|
+
const dest = path.join(ws.backupsDir, folder, path.relative(ws.dir, file));
|
|
80
|
+
await ensureDir(path.dirname(dest));
|
|
81
|
+
await fs.copyFile(file, dest);
|
|
82
|
+
return dest;
|
|
83
|
+
}
|
|
84
|
+
export const LOCAL_GITIGNORE = [
|
|
85
|
+
'# Managed by DevWorkspace. Machine-local state and regenerable output.',
|
|
86
|
+
'local/',
|
|
87
|
+
'analysis/onboarding-prompt.md',
|
|
88
|
+
'analysis/refresh-prompt.md',
|
|
89
|
+
'*.tmp',
|
|
90
|
+
'',
|
|
91
|
+
].join('\n');
|
|
92
|
+
//# sourceMappingURL=workspace.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace.js","sourceRoot":"","sources":["../../src/core/workspace.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { DevWorkspaceError } from '../errors/index.js';\nimport { atomicWriteJson, ensureDir, pathExists, readJsonFile } from '../utils/fs.js';\nimport { validateProject, type ProjectConfig, SCHEMA_VERSION } from '../config/schemas.js';\nimport { MIGRATIONS, migrateData, type Migration } from './migrations.js';\n\nexport const WORKSPACE_DIR = '.devworkspace';\n\nexport interface Ws {\n root: string;\n dir: string;\n projectJson: string;\n contextDir: string;\n facts: string;\n commands: string;\n decisionsDir: string;\n tasksActive: string;\n tasksCompleted: string;\n sessionsDir: string;\n analysisDir: string;\n discovery: string;\n onboardingPrompt: string;\n refreshPrompt: string;\n stateDir: string;\n analysisState: string;\n localDir: string;\n integrationsState: string;\n backupsDir: string;\n gitignore: string;\n}\n\nexport function wsPaths(root: string): Ws {\n const dir = path.join(root, WORKSPACE_DIR);\n const j = (...p: string[]): string => path.join(dir, ...p);\n return {\n root,\n dir,\n projectJson: j('project.json'),\n contextDir: j('context'),\n facts: j('context', 'facts.json'),\n commands: j('context', 'commands.json'),\n decisionsDir: j('decisions'),\n tasksActive: j('tasks', 'active'),\n tasksCompleted: j('tasks', 'completed'),\n sessionsDir: j('sessions'),\n analysisDir: j('analysis'),\n discovery: j('analysis', 'discovery.json'),\n onboardingPrompt: j('analysis', 'onboarding-prompt.md'),\n refreshPrompt: j('analysis', 'refresh-prompt.md'),\n stateDir: j('state'),\n analysisState: j('state', 'analysis.json'),\n localDir: j('local'),\n integrationsState: j('local', 'integrations.json'),\n backupsDir: j('local', 'backups'),\n gitignore: j('.gitignore'),\n };\n}\n\n/** Walk upward from start looking for an initialised workspace. The workspace location is never stored in it. */\nexport async function findWorkspaceRoot(start: string): Promise<string | null> {\n let dir = path.resolve(start);\n for (;;) {\n if (await pathExists(path.join(dir, WORKSPACE_DIR, 'project.json'))) return dir;\n const parent = path.dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\nexport async function requireWorkspace(cwd: string): Promise<Ws> {\n const root = await findWorkspaceRoot(cwd);\n if (!root) {\n throw new DevWorkspaceError('NOT_INITIALIZED', 'No DevWorkspace found in this directory or any parent.', {\n hint: 'Run \"devworkspace init\" in the project root.',\n });\n }\n return wsPaths(root);\n}\n\nexport async function loadProject(ws: Ws, migrations: Migration[] = MIGRATIONS.project ?? []): Promise<ProjectConfig> {\n const raw = await readJsonFile(ws.projectJson);\n const { data } = migrateData(raw, SCHEMA_VERSION, migrations, 'project.json');\n return validateProject(data);\n}\n\nexport async function saveProject(ws: Ws, cfg: ProjectConfig): Promise<void> {\n validateProject(cfg);\n await backupIfVersionChanges(ws, ws.projectJson, cfg.schemaVersion);\n await atomicWriteJson(ws.projectJson, cfg);\n}\n\n/** Before a write that upgrades a file's schemaVersion on disk, keep a copy of the old file. */\nexport async function backupIfVersionChanges(ws: Ws, file: string, newVersion: number, stamp?: string): Promise<string | null> {\n if (!(await pathExists(file))) return null;\n let onDisk: unknown;\n try {\n onDisk = ((await readJsonFile(file)) as { schemaVersion?: unknown }).schemaVersion;\n } catch {\n onDisk = undefined;\n }\n if (onDisk === newVersion) return null;\n const folder = stamp ?? new Date().toISOString().replace(/[:.]/g, '-');\n const dest = path.join(ws.backupsDir, folder, path.relative(ws.dir, file));\n await ensureDir(path.dirname(dest));\n await fs.copyFile(file, dest);\n return dest;\n}\n\nexport const LOCAL_GITIGNORE = [\n '# Managed by DevWorkspace. Machine-local state and regenerable output.',\n 'local/',\n 'analysis/onboarding-prompt.md',\n 'analysis/refresh-prompt.md',\n '*.tmp',\n '',\n].join('\\n');\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Ws } from '../core/workspace.js';
|
|
2
|
+
export type DecisionStatus = 'proposed' | 'accepted' | 'superseded';
|
|
3
|
+
export interface Decision {
|
|
4
|
+
number: number;
|
|
5
|
+
title: string;
|
|
6
|
+
status: DecisionStatus;
|
|
7
|
+
date: string;
|
|
8
|
+
supersedes?: number;
|
|
9
|
+
supersededBy?: number;
|
|
10
|
+
context: string;
|
|
11
|
+
decision: string;
|
|
12
|
+
consequences: string;
|
|
13
|
+
file: string;
|
|
14
|
+
}
|
|
15
|
+
export interface DecisionInput {
|
|
16
|
+
title: string;
|
|
17
|
+
context: string;
|
|
18
|
+
decision: string;
|
|
19
|
+
consequences: string;
|
|
20
|
+
status?: DecisionStatus;
|
|
21
|
+
supersedes?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function slugify(title: string): string;
|
|
24
|
+
export declare function renderDecision(d: Omit<Decision, 'file'>): string;
|
|
25
|
+
export declare function parseDecision(text: string, file: string): Decision | null;
|
|
26
|
+
export declare function listDecisions(ws: Ws): Promise<{
|
|
27
|
+
decisions: Decision[];
|
|
28
|
+
unreadable: string[];
|
|
29
|
+
}>;
|
|
30
|
+
/** Decisions are append-only: a new number is allocated and files are created exclusively, never overwritten. */
|
|
31
|
+
export declare function addDecision(ws: Ws, input: DecisionInput, now: string): Promise<Decision>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DevWorkspaceError } from '../errors/index.js';
|
|
4
|
+
import { ensureDir, pathExists } from '../utils/fs.js';
|
|
5
|
+
import { containsSecret } from '../security/redact.js';
|
|
6
|
+
export function slugify(title) {
|
|
7
|
+
const s = title
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.normalize('NFKD')
|
|
10
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
11
|
+
.replace(/^-+|-+$/g, '')
|
|
12
|
+
.slice(0, 60);
|
|
13
|
+
return s || 'decision';
|
|
14
|
+
}
|
|
15
|
+
export function renderDecision(d) {
|
|
16
|
+
const fm = [
|
|
17
|
+
'---',
|
|
18
|
+
`number: ${d.number}`,
|
|
19
|
+
`title: ${JSON.stringify(d.title)}`,
|
|
20
|
+
`status: ${d.status}`,
|
|
21
|
+
`date: ${d.date}`,
|
|
22
|
+
...(d.supersedes ? [`supersedes: ${d.supersedes}`] : []),
|
|
23
|
+
...(d.supersededBy ? [`supersededBy: ${d.supersededBy}`] : []),
|
|
24
|
+
'---',
|
|
25
|
+
];
|
|
26
|
+
return [...fm, '', `# ${String(d.number).padStart(4, '0')}. ${d.title}`, '', '## Context', d.context, '', '## Decision', d.decision, '', '## Consequences', d.consequences, ''].join('\n');
|
|
27
|
+
}
|
|
28
|
+
export function parseDecision(text, file) {
|
|
29
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/.exec(text);
|
|
30
|
+
if (!m)
|
|
31
|
+
return null;
|
|
32
|
+
const fm = {};
|
|
33
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
34
|
+
const kv = /^(\w+):\s*(.*)$/.exec(line);
|
|
35
|
+
if (kv)
|
|
36
|
+
fm[kv[1]] = kv[2];
|
|
37
|
+
}
|
|
38
|
+
const number = Number(fm.number);
|
|
39
|
+
if (!Number.isInteger(number) || !fm.title || !fm.status || !fm.date)
|
|
40
|
+
return null;
|
|
41
|
+
let title;
|
|
42
|
+
try {
|
|
43
|
+
title = JSON.parse(fm.title);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
title = fm.title;
|
|
47
|
+
}
|
|
48
|
+
const body = m[2];
|
|
49
|
+
const section = (name) => {
|
|
50
|
+
const r = new RegExp(`## ${name}\\r?\\n([\\s\\S]*?)(?=\\r?\\n## |$)`).exec(body);
|
|
51
|
+
return (r?.[1] ?? '').trim();
|
|
52
|
+
};
|
|
53
|
+
const status = fm.status;
|
|
54
|
+
if (!['proposed', 'accepted', 'superseded'].includes(status))
|
|
55
|
+
return null;
|
|
56
|
+
return {
|
|
57
|
+
number,
|
|
58
|
+
title,
|
|
59
|
+
status,
|
|
60
|
+
date: fm.date,
|
|
61
|
+
...(fm.supersedes ? { supersedes: Number(fm.supersedes) } : {}),
|
|
62
|
+
...(fm.supersededBy ? { supersededBy: Number(fm.supersededBy) } : {}),
|
|
63
|
+
context: section('Context'),
|
|
64
|
+
decision: section('Decision'),
|
|
65
|
+
consequences: section('Consequences'),
|
|
66
|
+
file,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export async function listDecisions(ws) {
|
|
70
|
+
const decisions = [];
|
|
71
|
+
const unreadable = [];
|
|
72
|
+
if (!(await pathExists(ws.decisionsDir)))
|
|
73
|
+
return { decisions, unreadable };
|
|
74
|
+
for (const name of (await fs.readdir(ws.decisionsDir)).filter((n) => n.endsWith('.md')).sort()) {
|
|
75
|
+
const d = parseDecision(await fs.readFile(path.join(ws.decisionsDir, name), 'utf8'), name);
|
|
76
|
+
if (d)
|
|
77
|
+
decisions.push(d);
|
|
78
|
+
else
|
|
79
|
+
unreadable.push(name);
|
|
80
|
+
}
|
|
81
|
+
return { decisions, unreadable };
|
|
82
|
+
}
|
|
83
|
+
/** Decisions are append-only: a new number is allocated and files are created exclusively, never overwritten. */
|
|
84
|
+
export async function addDecision(ws, input, now) {
|
|
85
|
+
for (const [k, v] of Object.entries({ title: input.title, context: input.context, decision: input.decision, consequences: input.consequences })) {
|
|
86
|
+
if (!v.trim())
|
|
87
|
+
throw new DevWorkspaceError('USAGE', `Decision ${k} must not be empty.`);
|
|
88
|
+
}
|
|
89
|
+
if (containsSecret(JSON.stringify(input))) {
|
|
90
|
+
throw new DevWorkspaceError('SECURITY', 'Decision text looks like it contains a secret; refusing to store it.');
|
|
91
|
+
}
|
|
92
|
+
await ensureDir(ws.decisionsDir);
|
|
93
|
+
const { decisions } = await listDecisions(ws);
|
|
94
|
+
if (input.supersedes !== undefined && !decisions.some((d) => d.number === input.supersedes)) {
|
|
95
|
+
throw new DevWorkspaceError('NOT_FOUND', `Decision ${input.supersedes} does not exist, so it cannot be superseded.`);
|
|
96
|
+
}
|
|
97
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
98
|
+
const number = Math.max(0, ...decisions.map((d) => d.number)) + 1 + attempt;
|
|
99
|
+
const base = { number, title: input.title.trim(), status: input.status ?? 'accepted', date: now.slice(0, 10), context: input.context.trim(), decision: input.decision.trim(), consequences: input.consequences.trim(), ...(input.supersedes ? { supersedes: input.supersedes } : {}) };
|
|
100
|
+
const file = `${String(number).padStart(4, '0')}-${slugify(input.title)}.md`;
|
|
101
|
+
try {
|
|
102
|
+
await fs.writeFile(path.join(ws.decisionsDir, file), renderDecision(base), { flag: 'wx' });
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
if (e.code === 'EEXIST')
|
|
106
|
+
continue;
|
|
107
|
+
throw new DevWorkspaceError('IO', 'Could not write the decision file.', { cause: e });
|
|
108
|
+
}
|
|
109
|
+
if (input.supersedes !== undefined) {
|
|
110
|
+
const old = decisions.find((d) => d.number === input.supersedes);
|
|
111
|
+
const oldPath = path.join(ws.decisionsDir, old.file);
|
|
112
|
+
const text = await fs.readFile(oldPath, 'utf8');
|
|
113
|
+
const updated = renderDecision({ ...old, status: 'superseded', supersededBy: number });
|
|
114
|
+
if (parseDecision(updated, old.file))
|
|
115
|
+
await fs.writeFile(oldPath, updated);
|
|
116
|
+
else
|
|
117
|
+
await fs.writeFile(oldPath, text);
|
|
118
|
+
}
|
|
119
|
+
return { ...base, file };
|
|
120
|
+
}
|
|
121
|
+
throw new DevWorkspaceError('CONFLICT', 'Could not allocate a decision number.', { hint: 'Retry the command.' });
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=decision.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decision.js","sourceRoot":"","sources":["../../src/decisions/decision.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { DevWorkspaceError } from '../errors/index.js';\nimport { ensureDir, pathExists } from '../utils/fs.js';\nimport type { Ws } from '../core/workspace.js';\nimport { containsSecret } from '../security/redact.js';\n\nexport type DecisionStatus = 'proposed' | 'accepted' | 'superseded';\n\nexport interface Decision {\n number: number;\n title: string;\n status: DecisionStatus;\n date: string;\n supersedes?: number;\n supersededBy?: number;\n context: string;\n decision: string;\n consequences: string;\n file: string;\n}\n\nexport interface DecisionInput {\n title: string;\n context: string;\n decision: string;\n consequences: string;\n status?: DecisionStatus;\n supersedes?: number;\n}\n\nexport function slugify(title: string): string {\n const s = title\n .toLowerCase()\n .normalize('NFKD')\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 60);\n return s || 'decision';\n}\n\nexport function renderDecision(d: Omit<Decision, 'file'>): string {\n const fm = [\n '---',\n `number: ${d.number}`,\n `title: ${JSON.stringify(d.title)}`,\n `status: ${d.status}`,\n `date: ${d.date}`,\n ...(d.supersedes ? [`supersedes: ${d.supersedes}`] : []),\n ...(d.supersededBy ? [`supersededBy: ${d.supersededBy}`] : []),\n '---',\n ];\n return [...fm, '', `# ${String(d.number).padStart(4, '0')}. ${d.title}`, '', '## Context', d.context, '', '## Decision', d.decision, '', '## Consequences', d.consequences, ''].join('\\n');\n}\n\nexport function parseDecision(text: string, file: string): Decision | null {\n const m = /^---\\r?\\n([\\s\\S]*?)\\r?\\n---\\r?\\n([\\s\\S]*)$/.exec(text);\n if (!m) return null;\n const fm: Record<string, string> = {};\n for (const line of m[1]!.split(/\\r?\\n/)) {\n const kv = /^(\\w+):\\s*(.*)$/.exec(line);\n if (kv) fm[kv[1]!] = kv[2]!;\n }\n const number = Number(fm.number);\n if (!Number.isInteger(number) || !fm.title || !fm.status || !fm.date) return null;\n let title: string;\n try {\n title = JSON.parse(fm.title) as string;\n } catch {\n title = fm.title;\n }\n const body = m[2]!;\n const section = (name: string): string => {\n const r = new RegExp(`## ${name}\\\\r?\\\\n([\\\\s\\\\S]*?)(?=\\\\r?\\\\n## |$)`).exec(body);\n return (r?.[1] ?? '').trim();\n };\n const status = fm.status as DecisionStatus;\n if (!['proposed', 'accepted', 'superseded'].includes(status)) return null;\n return {\n number,\n title,\n status,\n date: fm.date,\n ...(fm.supersedes ? { supersedes: Number(fm.supersedes) } : {}),\n ...(fm.supersededBy ? { supersededBy: Number(fm.supersededBy) } : {}),\n context: section('Context'),\n decision: section('Decision'),\n consequences: section('Consequences'),\n file,\n };\n}\n\nexport async function listDecisions(ws: Ws): Promise<{ decisions: Decision[]; unreadable: string[] }> {\n const decisions: Decision[] = [];\n const unreadable: string[] = [];\n if (!(await pathExists(ws.decisionsDir))) return { decisions, unreadable };\n for (const name of (await fs.readdir(ws.decisionsDir)).filter((n) => n.endsWith('.md')).sort()) {\n const d = parseDecision(await fs.readFile(path.join(ws.decisionsDir, name), 'utf8'), name);\n if (d) decisions.push(d);\n else unreadable.push(name);\n }\n return { decisions, unreadable };\n}\n\n/** Decisions are append-only: a new number is allocated and files are created exclusively, never overwritten. */\nexport async function addDecision(ws: Ws, input: DecisionInput, now: string): Promise<Decision> {\n for (const [k, v] of Object.entries({ title: input.title, context: input.context, decision: input.decision, consequences: input.consequences })) {\n if (!v.trim()) throw new DevWorkspaceError('USAGE', `Decision ${k} must not be empty.`);\n }\n if (containsSecret(JSON.stringify(input))) {\n throw new DevWorkspaceError('SECURITY', 'Decision text looks like it contains a secret; refusing to store it.');\n }\n await ensureDir(ws.decisionsDir);\n const { decisions } = await listDecisions(ws);\n if (input.supersedes !== undefined && !decisions.some((d) => d.number === input.supersedes)) {\n throw new DevWorkspaceError('NOT_FOUND', `Decision ${input.supersedes} does not exist, so it cannot be superseded.`);\n }\n for (let attempt = 0; attempt < 5; attempt++) {\n const number = Math.max(0, ...decisions.map((d) => d.number)) + 1 + attempt;\n const base = { number, title: input.title.trim(), status: input.status ?? 'accepted', date: now.slice(0, 10), context: input.context.trim(), decision: input.decision.trim(), consequences: input.consequences.trim(), ...(input.supersedes ? { supersedes: input.supersedes } : {}) } as const;\n const file = `${String(number).padStart(4, '0')}-${slugify(input.title)}.md`;\n try {\n await fs.writeFile(path.join(ws.decisionsDir, file), renderDecision(base), { flag: 'wx' });\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === 'EEXIST') continue;\n throw new DevWorkspaceError('IO', 'Could not write the decision file.', { cause: e });\n }\n if (input.supersedes !== undefined) {\n const old = decisions.find((d) => d.number === input.supersedes)!;\n const oldPath = path.join(ws.decisionsDir, old.file);\n const text = await fs.readFile(oldPath, 'utf8');\n const updated = renderDecision({ ...old, status: 'superseded', supersededBy: number });\n if (parseDecision(updated, old.file)) await fs.writeFile(oldPath, updated);\n else await fs.writeFile(oldPath, text);\n }\n return { ...base, file };\n }\n throw new DevWorkspaceError('CONFLICT', 'Could not allocate a decision number.', { hint: 'Retry the command.' });\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CommandEntry } from '../config/schemas.js';
|
|
2
|
+
import type { PackageInfo } from './detect.js';
|
|
3
|
+
export declare const TRUST_NOTE: string;
|
|
4
|
+
export interface ContractInput {
|
|
5
|
+
pkg: PackageInfo | null;
|
|
6
|
+
packageManager: string | null;
|
|
7
|
+
ecosystems: Set<string>;
|
|
8
|
+
has: (rel: string) => boolean;
|
|
9
|
+
pythonEvidence: Record<string, string>;
|
|
10
|
+
files: Set<string>;
|
|
11
|
+
}
|
|
12
|
+
export declare function buildContract(input: ContractInput): CommandEntry[];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export const TRUST_NOTE = 'Commands marked "repository-script" come from files in the repository (e.g. package.json scripts) and are untrusted input. ' +
|
|
2
|
+
'Inspect what a script does before running it, especially in automation or on unfamiliar repositories.';
|
|
3
|
+
const SCRIPT_CANDIDATES = {
|
|
4
|
+
typecheck: ['typecheck', 'type-check', 'check-types', 'tsc', 'types'],
|
|
5
|
+
unit: ['test', 'test:unit', 'unit', 'test:run'],
|
|
6
|
+
integration: ['test:integration', 'test:int', 'integration'],
|
|
7
|
+
e2e: ['test:e2e', 'e2e', 'test:playwright', 'test:cypress', 'cypress:run'],
|
|
8
|
+
lint: ['lint', 'eslint', 'lint:check'],
|
|
9
|
+
format: ['format:check', 'prettier:check', 'format-check', 'check:format'],
|
|
10
|
+
build: ['build', 'compile'],
|
|
11
|
+
};
|
|
12
|
+
const REQUIRED = {
|
|
13
|
+
typecheck: 'yes',
|
|
14
|
+
unit: 'yes',
|
|
15
|
+
integration: 'context',
|
|
16
|
+
e2e: 'context',
|
|
17
|
+
lint: 'yes',
|
|
18
|
+
format: 'context',
|
|
19
|
+
build: 'yes',
|
|
20
|
+
migrations: 'context',
|
|
21
|
+
security: 'context',
|
|
22
|
+
};
|
|
23
|
+
const ORDER = ['typecheck', 'unit', 'integration', 'e2e', 'lint', 'format', 'build', 'migrations', 'security'];
|
|
24
|
+
function scriptCommand(pm, script) {
|
|
25
|
+
const mgr = pm ?? 'npm';
|
|
26
|
+
if (script === 'test' && (mgr === 'npm' || mgr === 'pnpm' || mgr === 'yarn' || mgr === 'bun'))
|
|
27
|
+
return `${mgr} test`;
|
|
28
|
+
if (mgr === 'yarn')
|
|
29
|
+
return `yarn ${script}`;
|
|
30
|
+
if (mgr === 'bun')
|
|
31
|
+
return `bun run ${script}`;
|
|
32
|
+
return `${mgr} run ${script}`;
|
|
33
|
+
}
|
|
34
|
+
/** Placeholder scripts such as npm's default "no test specified" do not count as tests. */
|
|
35
|
+
function isRealScript(body) {
|
|
36
|
+
return !/no test specified/i.test(body) && body.trim() !== '';
|
|
37
|
+
}
|
|
38
|
+
export function buildContract(input) {
|
|
39
|
+
const found = new Map();
|
|
40
|
+
const set = (id, command, source, origin) => {
|
|
41
|
+
if (!found.has(id))
|
|
42
|
+
found.set(id, { id, available: true, command, required: REQUIRED[id], source, origin });
|
|
43
|
+
};
|
|
44
|
+
if (input.pkg) {
|
|
45
|
+
for (const [id, names] of Object.entries(SCRIPT_CANDIDATES)) {
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
const body = input.pkg.scripts[name];
|
|
48
|
+
if (body !== undefined && isRealScript(body)) {
|
|
49
|
+
set(id, scriptCommand(input.packageManager, name), `${input.pkg.rel}#scripts.${name}`, 'repository-script');
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!found.has('typecheck') && input.has('tsconfig.json') && input.pkg.hasTypes) {
|
|
55
|
+
set('typecheck', `${input.packageManager === 'pnpm' ? 'pnpm exec' : 'npx'} tsc --noEmit`, 'tsconfig.json', 'inferred');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (input.ecosystems.has('python')) {
|
|
59
|
+
const ev = input.pythonEvidence;
|
|
60
|
+
const hasTestsDir = [...input.files].some((f) => /^tests?\//.test(f) && f.endsWith('.py'));
|
|
61
|
+
if (ev.pytest || hasTestsDir || input.has('pytest.ini'))
|
|
62
|
+
set('unit', 'pytest', ev.pytest ?? 'tests/', 'inferred');
|
|
63
|
+
if (ev.ruff)
|
|
64
|
+
set('lint', 'ruff check .', ev.ruff, 'inferred');
|
|
65
|
+
if (ev.mypy)
|
|
66
|
+
set('typecheck', 'mypy .', ev.mypy, 'inferred');
|
|
67
|
+
if (ev.black)
|
|
68
|
+
set('format', 'black --check .', ev.black, 'inferred');
|
|
69
|
+
if (ev.alembic)
|
|
70
|
+
set('migrations', 'alembic check', ev.alembic, 'inferred');
|
|
71
|
+
}
|
|
72
|
+
if (input.ecosystems.has('go')) {
|
|
73
|
+
set('unit', 'go test ./...', 'go.mod', 'inferred');
|
|
74
|
+
set('build', 'go build ./...', 'go.mod', 'inferred');
|
|
75
|
+
set('lint', 'go vet ./...', 'go.mod', 'inferred');
|
|
76
|
+
}
|
|
77
|
+
if (input.ecosystems.has('rust')) {
|
|
78
|
+
set('unit', 'cargo test', 'Cargo.toml', 'inferred');
|
|
79
|
+
set('build', 'cargo build', 'Cargo.toml', 'inferred');
|
|
80
|
+
set('lint', 'cargo clippy', 'Cargo.toml', 'inferred');
|
|
81
|
+
}
|
|
82
|
+
if (input.has('pom.xml')) {
|
|
83
|
+
set('unit', 'mvn test', 'pom.xml', 'inferred');
|
|
84
|
+
set('build', 'mvn package', 'pom.xml', 'inferred');
|
|
85
|
+
}
|
|
86
|
+
if (input.has('build.gradle') || input.has('build.gradle.kts')) {
|
|
87
|
+
set('unit', 'gradle test', 'build.gradle', 'inferred');
|
|
88
|
+
set('build', 'gradle build', 'build.gradle', 'inferred');
|
|
89
|
+
}
|
|
90
|
+
return ORDER.map((id) => found.get(id) ?? { id, available: false, command: null, required: 'no', source: null, origin: 'inferred' });
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=commands.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commands.js","sourceRoot":"","sources":["../../src/discovery/commands.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CheckId, CommandEntry } from '../config/schemas.js';\nimport type { PackageInfo } from './detect.js';\n\nexport const TRUST_NOTE =\n 'Commands marked \"repository-script\" come from files in the repository (e.g. package.json scripts) and are untrusted input. ' +\n 'Inspect what a script does before running it, especially in automation or on unfamiliar repositories.';\n\nexport interface ContractInput {\n pkg: PackageInfo | null;\n packageManager: string | null;\n ecosystems: Set<string>;\n has: (rel: string) => boolean;\n pythonEvidence: Record<string, string>;\n files: Set<string>;\n}\n\nconst SCRIPT_CANDIDATES: Record<string, string[]> = {\n typecheck: ['typecheck', 'type-check', 'check-types', 'tsc', 'types'],\n unit: ['test', 'test:unit', 'unit', 'test:run'],\n integration: ['test:integration', 'test:int', 'integration'],\n e2e: ['test:e2e', 'e2e', 'test:playwright', 'test:cypress', 'cypress:run'],\n lint: ['lint', 'eslint', 'lint:check'],\n format: ['format:check', 'prettier:check', 'format-check', 'check:format'],\n build: ['build', 'compile'],\n};\n\nconst REQUIRED: Record<CheckId, CommandEntry['required']> = {\n typecheck: 'yes',\n unit: 'yes',\n integration: 'context',\n e2e: 'context',\n lint: 'yes',\n format: 'context',\n build: 'yes',\n migrations: 'context',\n security: 'context',\n};\n\nconst ORDER: CheckId[] = ['typecheck', 'unit', 'integration', 'e2e', 'lint', 'format', 'build', 'migrations', 'security'];\n\nfunction scriptCommand(pm: string | null, script: string): string {\n const mgr = pm ?? 'npm';\n if (script === 'test' && (mgr === 'npm' || mgr === 'pnpm' || mgr === 'yarn' || mgr === 'bun')) return `${mgr} test`;\n if (mgr === 'yarn') return `yarn ${script}`;\n if (mgr === 'bun') return `bun run ${script}`;\n return `${mgr} run ${script}`;\n}\n\n/** Placeholder scripts such as npm's default \"no test specified\" do not count as tests. */\nfunction isRealScript(body: string): boolean {\n return !/no test specified/i.test(body) && body.trim() !== '';\n}\n\nexport function buildContract(input: ContractInput): CommandEntry[] {\n const found = new Map<CheckId, CommandEntry>();\n const set = (id: CheckId, command: string, source: string, origin: CommandEntry['origin']): void => {\n if (!found.has(id)) found.set(id, { id, available: true, command, required: REQUIRED[id], source, origin });\n };\n\n if (input.pkg) {\n for (const [id, names] of Object.entries(SCRIPT_CANDIDATES)) {\n for (const name of names) {\n const body = input.pkg.scripts[name];\n if (body !== undefined && isRealScript(body)) {\n set(id as CheckId, scriptCommand(input.packageManager, name), `${input.pkg.rel}#scripts.${name}`, 'repository-script');\n break;\n }\n }\n }\n if (!found.has('typecheck') && input.has('tsconfig.json') && input.pkg.hasTypes) {\n set('typecheck', `${input.packageManager === 'pnpm' ? 'pnpm exec' : 'npx'} tsc --noEmit`, 'tsconfig.json', 'inferred');\n }\n }\n\n if (input.ecosystems.has('python')) {\n const ev = input.pythonEvidence;\n const hasTestsDir = [...input.files].some((f) => /^tests?\\//.test(f) && f.endsWith('.py'));\n if (ev.pytest || hasTestsDir || input.has('pytest.ini')) set('unit', 'pytest', ev.pytest ?? 'tests/', 'inferred');\n if (ev.ruff) set('lint', 'ruff check .', ev.ruff, 'inferred');\n if (ev.mypy) set('typecheck', 'mypy .', ev.mypy, 'inferred');\n if (ev.black) set('format', 'black --check .', ev.black, 'inferred');\n if (ev.alembic) set('migrations', 'alembic check', ev.alembic, 'inferred');\n }\n if (input.ecosystems.has('go')) {\n set('unit', 'go test ./...', 'go.mod', 'inferred');\n set('build', 'go build ./...', 'go.mod', 'inferred');\n set('lint', 'go vet ./...', 'go.mod', 'inferred');\n }\n if (input.ecosystems.has('rust')) {\n set('unit', 'cargo test', 'Cargo.toml', 'inferred');\n set('build', 'cargo build', 'Cargo.toml', 'inferred');\n set('lint', 'cargo clippy', 'Cargo.toml', 'inferred');\n }\n if (input.has('pom.xml')) {\n set('unit', 'mvn test', 'pom.xml', 'inferred');\n set('build', 'mvn package', 'pom.xml', 'inferred');\n }\n if (input.has('build.gradle') || input.has('build.gradle.kts')) {\n set('unit', 'gradle test', 'build.gradle', 'inferred');\n set('build', 'gradle build', 'build.gradle', 'inferred');\n }\n\n return ORDER.map(\n (id) => found.get(id) ?? { id, available: false, command: null, required: 'no' as const, source: null, origin: 'inferred' as const },\n );\n}\n"]}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ScanResult } from './scan.js';
|
|
2
|
+
import type { CommandEntry } from '../config/schemas.js';
|
|
3
|
+
export interface Detected {
|
|
4
|
+
name: string;
|
|
5
|
+
category: 'framework' | 'test' | 'tool' | 'database' | 'runtime';
|
|
6
|
+
evidence: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SubProject {
|
|
9
|
+
path: string;
|
|
10
|
+
name: string;
|
|
11
|
+
ecosystem: string;
|
|
12
|
+
frameworks: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface PackageInfo {
|
|
15
|
+
rel: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
scripts: Record<string, string>;
|
|
18
|
+
deps: Record<string, string>;
|
|
19
|
+
workspaces: string[];
|
|
20
|
+
packageManagerField?: string;
|
|
21
|
+
hasTypes: boolean;
|
|
22
|
+
type?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface DiscoveryReport {
|
|
25
|
+
schemaVersion: 1;
|
|
26
|
+
project: {
|
|
27
|
+
name: string;
|
|
28
|
+
};
|
|
29
|
+
scan: {
|
|
30
|
+
fileCount: number;
|
|
31
|
+
dirCount: number;
|
|
32
|
+
truncated: boolean;
|
|
33
|
+
skippedSymlinks: number;
|
|
34
|
+
ignoredEntries: number;
|
|
35
|
+
};
|
|
36
|
+
languages: {
|
|
37
|
+
name: string;
|
|
38
|
+
files: number;
|
|
39
|
+
}[];
|
|
40
|
+
topDirs: {
|
|
41
|
+
name: string;
|
|
42
|
+
files: number;
|
|
43
|
+
}[];
|
|
44
|
+
ecosystems: string[];
|
|
45
|
+
packageManager: string | null;
|
|
46
|
+
frameworks: Detected[];
|
|
47
|
+
monorepo: {
|
|
48
|
+
tool: string | null;
|
|
49
|
+
packages: SubProject[];
|
|
50
|
+
};
|
|
51
|
+
commands: CommandEntry[];
|
|
52
|
+
testConfigs: string[];
|
|
53
|
+
ci: string[];
|
|
54
|
+
docker: string[];
|
|
55
|
+
deployment: string[];
|
|
56
|
+
aiInstructions: string[];
|
|
57
|
+
docs: string[];
|
|
58
|
+
/** Names of credential-like files that exist. Their content is never read. */
|
|
59
|
+
sensitiveFilesPresent: string[];
|
|
60
|
+
entryHints: string[];
|
|
61
|
+
warnings: string[];
|
|
62
|
+
}
|
|
63
|
+
export declare function isAuxPath(rel: string): boolean;
|
|
64
|
+
export declare function readPackageJson(root: string, rel: string): Promise<PackageInfo | null>;
|
|
65
|
+
/** Expand simple workspace globs ("packages/*", "apps/**" treated as one level, or a literal directory). */
|
|
66
|
+
export declare function expandWorkspaceGlobs(patterns: string[], files: Set<string>, manifest: string): string[];
|
|
67
|
+
export interface DetectInput {
|
|
68
|
+
root: string;
|
|
69
|
+
projectName: string;
|
|
70
|
+
scan: ScanResult;
|
|
71
|
+
}
|
|
72
|
+
export declare function detectProject(input: DetectInput): Promise<DiscoveryReport>;
|