@captain_z/zsk 1.8.4 → 1.8.5
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/dist/bin.js +13 -0
- package/dist/bin.js.map +1 -1
- package/dist/commands/check.js +14 -575
- package/dist/commands/check.js.map +1 -1
- package/dist/commands/config.js +1 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/demo.d.ts +5 -0
- package/dist/commands/demo.js +70 -297
- package/dist/commands/demo.js.map +1 -1
- package/dist/commands/doctor.js +9 -4
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/gate.d.ts +1 -0
- package/dist/commands/gate.js +8 -2
- package/dist/commands/gate.js.map +1 -1
- package/dist/commands/prepare.js +7 -1
- package/dist/commands/prepare.js.map +1 -1
- package/dist/commands/project-init.js +30 -8
- package/dist/commands/project-init.js.map +1 -1
- package/dist/core/config.d.ts +68 -0
- package/dist/core/config.js +198 -15
- package/dist/core/config.js.map +1 -1
- package/dist/core/demo-auth.d.ts +30 -0
- package/dist/core/demo-auth.js +213 -0
- package/dist/core/demo-auth.js.map +1 -0
- package/dist/core/demo-scenarios.d.ts +62 -0
- package/dist/core/demo-scenarios.js +276 -0
- package/dist/core/demo-scenarios.js.map +1 -0
- package/dist/core/demo-sources.d.ts +37 -0
- package/dist/core/demo-sources.js +198 -0
- package/dist/core/demo-sources.js.map +1 -0
- package/dist/core/mcp-registry-discovery.d.ts +16 -0
- package/dist/core/mcp-registry-discovery.js +187 -0
- package/dist/core/mcp-registry-discovery.js.map +1 -0
- package/dist/core/origin-detection.js +1 -1
- package/dist/core/origin-detection.js.map +1 -1
- package/dist/core/prepare-artifacts.d.ts +16 -0
- package/dist/core/prepare-artifacts.js +25 -0
- package/dist/core/prepare-artifacts.js.map +1 -0
- package/dist/core/prepare-auth-helper.d.ts +8 -0
- package/dist/core/prepare-auth-helper.js +32 -0
- package/dist/core/prepare-auth-helper.js.map +1 -0
- package/dist/core/prepare-materialization.d.ts +8 -0
- package/dist/core/prepare-materialization.js +26 -0
- package/dist/core/prepare-materialization.js.map +1 -0
- package/dist/core/prepare-migration.d.ts +6 -0
- package/dist/core/prepare-migration.js +57 -0
- package/dist/core/prepare-migration.js.map +1 -0
- package/dist/core/prepare-reporting.d.ts +5 -0
- package/dist/core/prepare-reporting.js +106 -0
- package/dist/core/prepare-reporting.js.map +1 -0
- package/dist/core/prepare-routing.d.ts +12 -0
- package/dist/core/prepare-routing.js +182 -0
- package/dist/core/prepare-routing.js.map +1 -0
- package/dist/core/prepare-sync.d.ts +11 -22
- package/dist/core/prepare-sync.js +811 -260
- package/dist/core/prepare-sync.js.map +1 -1
- package/dist/core/prepare-utils.d.ts +6 -0
- package/dist/core/prepare-utils.js +35 -0
- package/dist/core/prepare-utils.js.map +1 -0
- package/dist/core/provider-policy.d.ts +26 -0
- package/dist/core/provider-policy.js +180 -0
- package/dist/core/provider-policy.js.map +1 -0
- package/dist/core/provider-readiness.d.ts +39 -0
- package/dist/core/provider-readiness.js +78 -0
- package/dist/core/provider-readiness.js.map +1 -0
- package/dist/core/source-adapter-normalization.d.ts +31 -0
- package/dist/core/source-adapter-normalization.js +235 -0
- package/dist/core/source-adapter-normalization.js.map +1 -0
- package/dist/core/source-snapshot-adapters.d.ts +3 -3
- package/dist/core/source-snapshot-adapters.js +2 -24
- package/dist/core/source-snapshot-adapters.js.map +1 -1
- package/dist/core/staffing-plan.d.ts +1 -0
- package/dist/core/staffing-plan.js +61 -3
- package/dist/core/staffing-plan.js.map +1 -1
- package/dist/core/stage-clarity-verification.js +21 -18
- package/dist/core/stage-clarity-verification.js.map +1 -1
- package/dist/core/stage-output-quality.d.ts +3 -0
- package/dist/core/stage-output-quality.js +122 -0
- package/dist/core/stage-output-quality.js.map +1 -0
- package/dist/core/stage-quality-contracts.d.ts +19 -0
- package/dist/core/stage-quality-contracts.js.map +1 -1
- package/dist/core/stage-quality-criteria.js +0 -37
- package/dist/core/stage-quality-criteria.js.map +1 -1
- package/dist/core/stage-quality-rendering.d.ts +4 -2
- package/dist/core/stage-quality-rendering.js +130 -12
- package/dist/core/stage-quality-rendering.js.map +1 -1
- package/dist/core/stage-quality.js +17 -6
- package/dist/core/stage-quality.js.map +1 -1
- package/dist/core/template-registry.js +12 -15
- package/dist/core/template-registry.js.map +1 -1
- package/dist/core/workspace-conformance.d.ts +39 -0
- package/dist/core/workspace-conformance.js +603 -0
- package/dist/core/workspace-conformance.js.map +1 -0
- package/package.json +2 -2
- package/schemas/providers.schema.json +74 -0
- package/schemas/zsk-config.schema.json +417 -1
- package/templates/project-init/.zsk/README.md +48 -0
- package/templates/project-init/.zsk/config.yaml +37 -33
- package/templates/project-init/.zsk/docs/CONFIG-SCHEMA.md +127 -0
- package/templates/project-init/.zsk/docs/PROJECT-CONFIG.md +26 -21
- package/templates/project-init/.zsk/docs/README.md +10 -0
- package/templates/project-init/.zsk/docs/SECURITY.md +34 -0
- package/templates/project-init/.zsk/docs/SYSTEM-SPEC.md +20 -8
- package/templates/project-init/.zsk/evidence/README.md +15 -0
- package/templates/project-init/.zsk/issues/README.md +10 -0
- package/templates/project-init/.zsk/modules/README.md +19 -0
- package/templates/project-init/.zsk/modules/index.md +9 -5
- package/templates/project-init/.zsk/raws/README.md +34 -0
- package/templates/project-init/.zsk/roles.yaml +36 -105
- package/templates/project-init/.zsk/templates/config.examples.yaml +83 -0
- package/templates/project-init/.zsk/templates/issue-card.md +23 -0
- package/templates/project-init/.zsk/templates/module/README.md +13 -0
- package/templates/project-init/.zsk/templates/module/design.md +22 -0
- package/templates/project-init/.zsk/templates/module/module.yaml +15 -0
- package/templates/project-init/.zsk/templates/module/proposal.md +20 -0
- package/templates/project-init/.zsk/templates/module/spec.md +22 -0
- package/templates/project-init/.zsk/templates/module/tasks.md +16 -0
- package/templates/project-init/.zsk/raws/index.md +0 -34
- package/templates/project-init/.zsk/raws/manifest.json +0 -4
- package/templates/project-init/.zsk/raws/prepare/backend/index.md +0 -4
- package/templates/project-init/.zsk/raws/prepare/design/index.md +0 -21
- package/templates/project-init/.zsk/raws/prepare/index.md +0 -37
- package/templates/project-init/.zsk/raws/prepare/product/index.md +0 -4
- package/templates/project-init/.zsk/raws/prepare/qa/index.md +0 -4
- package/templates/project-init/.zsk/raws/prepare/ux/index.md +0 -22
|
@@ -1,40 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
|
-
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname, extname,
|
|
5
|
-
import {
|
|
4
|
+
import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { DESIGN_LOCAL_FILE_ORIGIN_KEYS, JIRA_LOCAL_FILE_ORIGIN_KEYS, flattenPrepareEntries, resolveSourceSnapshot, } from "./config.js";
|
|
6
8
|
import { inferSourceOrigin } from "./origin-detection.js";
|
|
9
|
+
import { discoverMcpRegistries } from "./mcp-registry-discovery.js";
|
|
10
|
+
import { loadProviderPolicy } from "./provider-policy.js";
|
|
11
|
+
import { buildProviderReadinessEvidence } from "./provider-readiness.js";
|
|
7
12
|
import { describeResource, updateRawIndexes, updateRawManifest } from "./raw-manifest.js";
|
|
13
|
+
import { createSyncRunId, resolvePrepareSyncArtifacts, } from "./prepare-artifacts.js";
|
|
14
|
+
import { hashIfExists, sha256, writePreparedSnapshot } from "./prepare-materialization.js";
|
|
15
|
+
import { renderDownstreamImpact, renderPreparationReport } from "./prepare-reporting.js";
|
|
16
|
+
import { routePreparedResources } from "./prepare-routing.js";
|
|
17
|
+
import { escapeTable, exists, isRecord, safeFileName } from "./prepare-utils.js";
|
|
18
|
+
import { interpretSourceAdapter } from "./source-adapter-normalization.js";
|
|
8
19
|
import { acquireSourceSnapshot, chooseSourceSnapshotStrategy, createSourceSnapshotContext, } from "./source-snapshot-adapters.js";
|
|
20
|
+
import { isPathInside, resolvePlaywrightAuthProfilePath, resolvePlaywrightAuthStatePath, } from "./workspace-conformance.js";
|
|
9
21
|
import { getWorkspacePath } from "./workspace-layout.js";
|
|
10
|
-
export
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
adapterResultsDir: join(dir, "adapter-results"),
|
|
17
|
-
authCheckPath: join(dir, "auth-check.json"),
|
|
18
|
-
downstreamImpactPath: join(dir, "downstream-impact.md"),
|
|
19
|
-
authScriptPath: join(authDir, "login.mjs"),
|
|
20
|
-
authStatePath: join(authDir, "user_data.json"),
|
|
21
|
-
migrationPlanPath: join(dir, "raws-migration-plan.md"),
|
|
22
|
-
};
|
|
23
|
-
}
|
|
22
|
+
export { createSyncRunId, resolvePrepareSyncArtifacts } from "./prepare-artifacts.js";
|
|
23
|
+
export { writeAuthLoginHelper } from "./prepare-auth-helper.js";
|
|
24
|
+
export { buildRawMigrationPlan } from "./prepare-migration.js";
|
|
25
|
+
const execFileAsync = promisify(execFile);
|
|
26
|
+
const DEFAULT_ADAPTER_COMMAND_TIMEOUT_MS = 60_000;
|
|
27
|
+
const DEFAULT_ADAPTER_COMMAND_MAX_BUFFER = 10 * 1024 * 1024;
|
|
24
28
|
export async function syncPrepareSources(target, config, opts = {}) {
|
|
25
29
|
const runId = opts.runId ?? createSyncRunId();
|
|
26
30
|
const artifacts = resolvePrepareSyncArtifacts(target, config, runId);
|
|
27
|
-
const entries =
|
|
31
|
+
const entries = flattenPrepareEntries(config);
|
|
28
32
|
const selected = new Set(selectSourceEntries(entries, opts));
|
|
29
33
|
const results = [];
|
|
30
34
|
await mkdir(artifacts.adapterResultsDir, { recursive: true });
|
|
35
|
+
await mkdir(artifacts.providerReadinessDir, { recursive: true });
|
|
36
|
+
const providerReadiness = await resolveProviderReadiness(target, artifacts, [...selected]);
|
|
31
37
|
for (const entry of entries) {
|
|
32
38
|
if (!selected.has(entry))
|
|
33
39
|
continue;
|
|
34
|
-
const result = await syncEntry(target, config, entry, opts);
|
|
40
|
+
const result = annotateProviderReadiness(target, entry, await syncEntry(target, config, entry, opts, entries), providerReadiness.byProvider);
|
|
35
41
|
results.push(result);
|
|
36
42
|
await writeFile(join(artifacts.adapterResultsDir, `${safeFileName(entry.id)}.json`), `${JSON.stringify(result, null, 2)}\n`, "utf8");
|
|
37
43
|
}
|
|
44
|
+
const routes = opts.dryRun ? [] : await routePreparedResources(target, config, results);
|
|
38
45
|
if (!opts.dryRun) {
|
|
39
46
|
const records = [];
|
|
40
47
|
for (const entry of entries) {
|
|
@@ -46,43 +53,22 @@ export async function syncPrepareSources(target, config, opts = {}) {
|
|
|
46
53
|
const downstreamImpact = renderDownstreamImpact(results);
|
|
47
54
|
await mkdir(dirname(artifacts.downstreamImpactPath), { recursive: true });
|
|
48
55
|
await writeFile(artifacts.downstreamImpactPath, downstreamImpact, "utf8");
|
|
56
|
+
const syncReport = renderPreparationReport(entries, results, routes);
|
|
57
|
+
await mkdir(dirname(artifacts.syncReportPath), { recursive: true });
|
|
58
|
+
await writeFile(artifacts.syncReportPath, syncReport, "utf8");
|
|
49
59
|
return {
|
|
50
60
|
artifacts,
|
|
51
61
|
results,
|
|
62
|
+
routes,
|
|
63
|
+
providerReadiness: [...providerReadiness.byProvider.values()].map((item) => item.evidence),
|
|
52
64
|
downstreamImpact,
|
|
65
|
+
syncReport,
|
|
53
66
|
};
|
|
54
67
|
}
|
|
55
|
-
export async function writeAuthLoginHelper(target, config, opts = {}) {
|
|
56
|
-
const artifacts = resolvePrepareSyncArtifacts(target, config, opts.runId ?? createSyncRunId());
|
|
57
|
-
const statePath = opts.out ? resolve(target, opts.out) : resolveAuthProfilePath(target, config, opts.profile);
|
|
58
|
-
const authRoot = dirname(artifacts.authScriptPath);
|
|
59
|
-
if (!isInside(authRoot, statePath)) {
|
|
60
|
-
throw new Error(`Playwright storageState output must stay under ${authRoot}`);
|
|
61
|
-
}
|
|
62
|
-
const script = [
|
|
63
|
-
`import { chromium } from "playwright";`,
|
|
64
|
-
``,
|
|
65
|
-
`const targetUrl = process.env.ZSK_PREPARE_AUTH_URL ?? ${JSON.stringify(opts.url ?? "about:blank")};`,
|
|
66
|
-
`const storageStatePath = process.env.ZSK_PLAYWRIGHT_STORAGE_STATE ?? ${JSON.stringify(statePath)};`,
|
|
67
|
-
`const browser = await chromium.launch({ headless: false });`,
|
|
68
|
-
`const context = await browser.newContext();`,
|
|
69
|
-
`const page = await context.newPage();`,
|
|
70
|
-
`await page.goto(targetUrl, { waitUntil: "domcontentloaded" });`,
|
|
71
|
-
`console.log("Complete login in the opened browser, then press Enter here to save storageState.");`,
|
|
72
|
-
`await new Promise((resolve) => process.stdin.once("data", resolve));`,
|
|
73
|
-
`await context.storageState({ path: storageStatePath });`,
|
|
74
|
-
`console.log(\`Saved Playwright storageState to ${"${storageStatePath}"}\`);`,
|
|
75
|
-
`await browser.close();`,
|
|
76
|
-
``,
|
|
77
|
-
].join("\n");
|
|
78
|
-
await mkdir(dirname(artifacts.authScriptPath), { recursive: true });
|
|
79
|
-
await writeFile(artifacts.authScriptPath, script, "utf8");
|
|
80
|
-
return { ...artifacts, authStatePath: statePath };
|
|
81
|
-
}
|
|
82
68
|
export async function checkPrepareAuth(target, config, opts = {}) {
|
|
83
69
|
const runId = opts.runId ?? createSyncRunId();
|
|
84
70
|
const artifacts = resolvePrepareSyncArtifacts(target, config, runId);
|
|
85
|
-
const entries = selectSourceEntries(
|
|
71
|
+
const entries = selectSourceEntries(flattenPrepareEntries(config), opts);
|
|
86
72
|
const authStatePath = resolveSelectedAuthStatePath(target, config, opts);
|
|
87
73
|
const results = [];
|
|
88
74
|
for (const entry of entries) {
|
|
@@ -92,45 +78,6 @@ export async function checkPrepareAuth(target, config, opts = {}) {
|
|
|
92
78
|
await writeFile(artifacts.authCheckPath, `${JSON.stringify({ runId, results }, null, 2)}\n`, "utf8");
|
|
93
79
|
return { artifacts, results };
|
|
94
80
|
}
|
|
95
|
-
export async function buildRawMigrationPlan(target, config, runId = createSyncRunId()) {
|
|
96
|
-
const artifacts = resolvePrepareSyncArtifacts(target, config, runId);
|
|
97
|
-
const prepareRoot = resolve(target, getWorkspacePath(config, "resourcesRoot"), "prepare");
|
|
98
|
-
const staleNames = new Set([
|
|
99
|
-
"qa-engineer",
|
|
100
|
-
"test-engineer",
|
|
101
|
-
"backend-engineer",
|
|
102
|
-
"frontend-engineer",
|
|
103
|
-
"product-manager",
|
|
104
|
-
"designer",
|
|
105
|
-
"jira",
|
|
106
|
-
"confluence",
|
|
107
|
-
"figma",
|
|
108
|
-
"modao",
|
|
109
|
-
"manual",
|
|
110
|
-
"provider",
|
|
111
|
-
"version",
|
|
112
|
-
]);
|
|
113
|
-
const entries = await safeReadDir(prepareRoot);
|
|
114
|
-
const findings = entries
|
|
115
|
-
.filter((entry) => entry.isDirectory() && isMigrationLaneCandidate(entry.name, staleNames))
|
|
116
|
-
.map((entry) => ({
|
|
117
|
-
path: join(".zsk/raws/prepare", entry.name),
|
|
118
|
-
action: "dry-run only; keep readable, require explicit migration before rewriting references",
|
|
119
|
-
}));
|
|
120
|
-
const content = [
|
|
121
|
-
"# Raw Migration Plan",
|
|
122
|
-
"",
|
|
123
|
-
"Dry-run report only. No files were moved, deleted, or rewritten.",
|
|
124
|
-
"",
|
|
125
|
-
findings.length === 0
|
|
126
|
-
? "No stale provider/method/version prepare lanes were found."
|
|
127
|
-
: findings.map((item) => `- \`${item.path}\`: ${item.action}`).join("\n"),
|
|
128
|
-
"",
|
|
129
|
-
].join("\n");
|
|
130
|
-
await mkdir(dirname(artifacts.migrationPlanPath), { recursive: true });
|
|
131
|
-
await writeFile(artifacts.migrationPlanPath, content, "utf8");
|
|
132
|
-
return { artifacts, content };
|
|
133
|
-
}
|
|
134
81
|
function selectSourceEntries(entries, opts) {
|
|
135
82
|
if (opts.all || !opts.source)
|
|
136
83
|
return entries;
|
|
@@ -145,6 +92,52 @@ function selectSourceEntries(entries, opts) {
|
|
|
145
92
|
}
|
|
146
93
|
return alias;
|
|
147
94
|
}
|
|
95
|
+
async function resolveProviderReadiness(target, artifacts, entries) {
|
|
96
|
+
const names = new Map();
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const normalized = interpretSourceAdapter(entry.source);
|
|
99
|
+
if (!normalized.mcpProviderName)
|
|
100
|
+
continue;
|
|
101
|
+
names.set(normalized.mcpProviderName, normalized);
|
|
102
|
+
}
|
|
103
|
+
if (names.size === 0)
|
|
104
|
+
return { byProvider: new Map() };
|
|
105
|
+
const policy = await loadProviderPolicy(target);
|
|
106
|
+
const registry = await discoverMcpRegistries(target);
|
|
107
|
+
const byProvider = new Map();
|
|
108
|
+
for (const [providerName, normalized] of names) {
|
|
109
|
+
const evidence = buildProviderReadinessEvidence(target, providerName, normalized.adapterId, policy, registry.registrations);
|
|
110
|
+
evidence.warnings.push(...registry.warnings);
|
|
111
|
+
const path = join(artifacts.providerReadinessDir, `${safeFileName(providerName)}.json`);
|
|
112
|
+
await writeFile(path, `${JSON.stringify(evidence, null, 2)}\n`, "utf8");
|
|
113
|
+
byProvider.set(providerName, { path, evidence });
|
|
114
|
+
}
|
|
115
|
+
return { byProvider };
|
|
116
|
+
}
|
|
117
|
+
function annotateProviderReadiness(target, entry, result, readiness) {
|
|
118
|
+
const normalized = interpretSourceAdapter(entry.source);
|
|
119
|
+
const providerName = normalized.mcpProviderName;
|
|
120
|
+
if (!providerName)
|
|
121
|
+
return result;
|
|
122
|
+
const item = readiness.get(providerName);
|
|
123
|
+
if (!item)
|
|
124
|
+
return result;
|
|
125
|
+
return {
|
|
126
|
+
...result,
|
|
127
|
+
metadata: cleanMetadata({
|
|
128
|
+
...(result.metadata ?? {}),
|
|
129
|
+
adapterId: normalized.adapterId,
|
|
130
|
+
mcpProvider: providerName,
|
|
131
|
+
providerReadiness: relative(target, item.path),
|
|
132
|
+
providerReadinessStatus: item.evidence.summary,
|
|
133
|
+
providerPolicyStatus: item.evidence.policy.status,
|
|
134
|
+
providerRegistryStatus: item.evidence.registry.status,
|
|
135
|
+
providerConnectionStatus: item.evidence.connection.status,
|
|
136
|
+
capabilitiesMatched: item.evidence.capabilities.matched.join(", ") || undefined,
|
|
137
|
+
capabilitiesEvaluation: item.evidence.capabilities.evaluation,
|
|
138
|
+
}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
148
141
|
async function checkAuthEntry(entry, authStatePath, opts) {
|
|
149
142
|
const origin = inferSourceOrigin(entry.source);
|
|
150
143
|
const base = {
|
|
@@ -155,20 +148,20 @@ async function checkAuthEntry(entry, authStatePath, opts) {
|
|
|
155
148
|
origin: origin.ref,
|
|
156
149
|
method: origin.method,
|
|
157
150
|
};
|
|
158
|
-
const url = origin
|
|
159
|
-
if (!url
|
|
151
|
+
const url = authCheckUrlForSource(entry.source, origin);
|
|
152
|
+
if (!url) {
|
|
160
153
|
return {
|
|
161
154
|
...base,
|
|
162
155
|
status: "source-gap",
|
|
163
156
|
authSources: [],
|
|
164
157
|
directAuthAvailable: false,
|
|
165
158
|
networkAttempted: false,
|
|
166
|
-
reason: "auth-check
|
|
159
|
+
reason: "auth-check needs origin.url or provider fields that derive a REST URL before checking runtime credentials",
|
|
167
160
|
validation: { urlOrigin: "fail", secretValuesHidden: "pass" },
|
|
168
161
|
};
|
|
169
162
|
}
|
|
170
163
|
const { headers, authSources } = await headersForSource(entry.source, url, authStatePath);
|
|
171
|
-
const directAuthAvailable =
|
|
164
|
+
const directAuthAvailable = hasRuntimeAuthHeaders(headers);
|
|
172
165
|
if (!opts.allowNetwork) {
|
|
173
166
|
return {
|
|
174
167
|
...base,
|
|
@@ -254,17 +247,37 @@ async function checkAuthEntry(entry, authStatePath, opts) {
|
|
|
254
247
|
};
|
|
255
248
|
}
|
|
256
249
|
}
|
|
257
|
-
|
|
258
|
-
|
|
250
|
+
function authCheckUrlForSource(source, origin) {
|
|
251
|
+
if (origin.ref && origin.method === "url")
|
|
252
|
+
return origin.ref;
|
|
253
|
+
const provider = sourceProviderToken(source);
|
|
254
|
+
if (provider?.includes("confluence")) {
|
|
255
|
+
return firstOriginString(source, "apiUrl", "restUrl") ?? deriveConfluenceRestUrl(source);
|
|
256
|
+
}
|
|
257
|
+
if (provider?.includes("jira")) {
|
|
258
|
+
return firstOriginString(source, "apiUrl", "searchUrl") ?? deriveJiraRestUrl(source);
|
|
259
|
+
}
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
async function syncEntry(target, config, entry, opts, entries = [entry]) {
|
|
263
|
+
const context = await createSourceSnapshotContext(target, config, entry, optsWithSourceFallback(entry.source, opts), hashIfExists);
|
|
264
|
+
const sharedSnapshot = sharedSnapshotProvenance(target, config, entry, entries);
|
|
259
265
|
return acquireSourceSnapshot(context, {
|
|
260
266
|
dryRun: skippedDryRunSnapshot,
|
|
261
267
|
local: syncLocalSnapshot,
|
|
262
|
-
provider: syncProviderSnapshot,
|
|
268
|
+
provider: (snapshotContext, providerAdapter) => syncProviderSnapshot(snapshotContext, providerAdapter, sharedSnapshot),
|
|
263
269
|
repository: syncRepositorySnapshot,
|
|
264
270
|
url: syncUrlSnapshot,
|
|
265
271
|
providerManaged: blockedProviderManagedSnapshot,
|
|
266
272
|
});
|
|
267
273
|
}
|
|
274
|
+
function optsWithSourceFallback(source, opts) {
|
|
275
|
+
const interpretation = interpretSourceAdapter(source);
|
|
276
|
+
if (interpretation.hasBrowserFallback) {
|
|
277
|
+
return { ...opts, browser: true };
|
|
278
|
+
}
|
|
279
|
+
return opts;
|
|
280
|
+
}
|
|
268
281
|
function skippedDryRunSnapshot(context) {
|
|
269
282
|
return {
|
|
270
283
|
...context.base,
|
|
@@ -276,61 +289,91 @@ function skippedDryRunSnapshot(context) {
|
|
|
276
289
|
};
|
|
277
290
|
}
|
|
278
291
|
async function syncLocalSnapshot(context) {
|
|
279
|
-
const { base, entry, source, snapshotPath, previousSnapshotHash, target } = context;
|
|
292
|
+
const { base, entry, source, snapshotPath, previousSnapshotHash, target, opts } = context;
|
|
280
293
|
const sourcePath = source.origin?.path ?? source.path;
|
|
281
294
|
if (!sourcePath) {
|
|
282
295
|
return sourceGap(base, "configured local origin has no path");
|
|
283
296
|
}
|
|
284
297
|
const originPath = resolve(target, sourcePath);
|
|
285
|
-
if (!(await exists(originPath))) {
|
|
298
|
+
if (!(await exists(originPath)) && opts.allowNetwork && isGitSubmoduleResource(source)) {
|
|
299
|
+
const update = await updateGitSubmodule(target, sourcePath);
|
|
300
|
+
if (!update.ok)
|
|
301
|
+
return failed(base, update.reason);
|
|
302
|
+
}
|
|
303
|
+
const info = await safeStat(originPath);
|
|
304
|
+
if (!info) {
|
|
286
305
|
return sourceGap(base, "configured local origin path does not exist");
|
|
287
306
|
}
|
|
288
|
-
|
|
289
|
-
if (
|
|
307
|
+
let written;
|
|
308
|
+
if (info.isDirectory) {
|
|
309
|
+
written = await writePreparedSnapshot(target, snapshotPath, await renderStructuredLocalDirectoryMarkdown(target, entry, originPath), previousSnapshotHash);
|
|
310
|
+
}
|
|
311
|
+
else if (shouldPreserveMachineReadable(source, originPath, snapshotPath)) {
|
|
312
|
+
await mkdir(dirname(snapshotPath), { recursive: true });
|
|
290
313
|
await copyFile(originPath, snapshotPath);
|
|
314
|
+
const snapshotHash = await sha256(snapshotPath);
|
|
315
|
+
written = {
|
|
316
|
+
snapshotHash,
|
|
317
|
+
changed: previousSnapshotHash !== snapshotHash,
|
|
318
|
+
};
|
|
291
319
|
}
|
|
292
320
|
else {
|
|
293
|
-
await
|
|
321
|
+
written = await writePreparedSnapshot(target, snapshotPath, await renderStructuredLocalMarkdown(target, entry, originPath), previousSnapshotHash);
|
|
294
322
|
}
|
|
295
|
-
const snapshotHash = await sha256(snapshotPath);
|
|
296
323
|
return {
|
|
297
324
|
...base,
|
|
298
325
|
strategy: "local-copy-structured-markdown",
|
|
299
326
|
status: "materialized",
|
|
300
|
-
snapshotHash,
|
|
301
|
-
changed:
|
|
302
|
-
reason:
|
|
327
|
+
snapshotHash: written.snapshotHash,
|
|
328
|
+
changed: written.changed,
|
|
329
|
+
reason: info.isDirectory
|
|
330
|
+
? "local directory origin summarized into configured snapshot"
|
|
331
|
+
: "local origin copied or materialized into configured snapshot",
|
|
303
332
|
validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
304
333
|
};
|
|
305
334
|
}
|
|
306
|
-
async function syncProviderSnapshot(context, providerAdapter) {
|
|
335
|
+
async function syncProviderSnapshot(context, providerAdapter, sharedSnapshot) {
|
|
307
336
|
const { target, config, entry, snapshotPath, previousSnapshotHash, opts, origin } = context;
|
|
308
337
|
const authStatePath = resolveSelectedAuthStatePath(target, config, opts);
|
|
309
|
-
const adapterResult = await runProviderAdapter(providerAdapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
|
|
338
|
+
const adapterResult = await runProviderAdapter(providerAdapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
|
|
310
339
|
if (!adapterResult)
|
|
311
340
|
return null;
|
|
341
|
+
if (adapterResult.status === "source-gap" && origin.method === "url") {
|
|
342
|
+
if (adapterResult.metadata?.providerGap === "adapter-ambiguous") {
|
|
343
|
+
return adapterResult;
|
|
344
|
+
}
|
|
345
|
+
const fallback = await syncUrlSnapshot(context);
|
|
346
|
+
return annotateFallbackResult(fallback, providerAdapter, adapterResult);
|
|
347
|
+
}
|
|
312
348
|
if (adapterResult.status === "blocked-auth" && opts.browser && origin.method === "url") {
|
|
313
349
|
const browserResult = await fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
|
|
314
|
-
return {
|
|
315
|
-
...browserResult,
|
|
316
|
-
adapter: `${providerAdapter}-browser-fallback`,
|
|
317
|
-
metadata: cleanMetadata({ providerAdapter, fallbackFrom: adapterResult.strategy }),
|
|
318
|
-
};
|
|
350
|
+
return annotateFallbackResult(browserResult, providerAdapter, adapterResult, `${providerAdapter}-browser-fallback`);
|
|
319
351
|
}
|
|
320
352
|
return adapterResult;
|
|
321
353
|
}
|
|
354
|
+
function annotateFallbackResult(fallback, providerAdapter, adapterResult, adapter = `${providerAdapter}-generic-fallback`) {
|
|
355
|
+
return {
|
|
356
|
+
...fallback,
|
|
357
|
+
adapter,
|
|
358
|
+
metadata: cleanMetadata({
|
|
359
|
+
...(fallback.metadata ?? {}),
|
|
360
|
+
providerAdapter,
|
|
361
|
+
fallbackFrom: adapterResult.strategy,
|
|
362
|
+
fallbackReason: adapterResult.reason,
|
|
363
|
+
fallbackStatus: adapterResult.status,
|
|
364
|
+
}),
|
|
365
|
+
};
|
|
366
|
+
}
|
|
322
367
|
async function syncRepositorySnapshot(context) {
|
|
323
368
|
const { base, entry, origin, snapshotPath, previousSnapshotHash } = context;
|
|
324
|
-
await mkdir(dirname(snapshotPath), { recursive: true });
|
|
325
369
|
const content = renderRepositoryMetadata(entry, origin.ref ?? "");
|
|
326
|
-
await
|
|
327
|
-
const snapshotHash = await sha256(snapshotPath);
|
|
370
|
+
const written = await writePreparedSnapshot(context.target, snapshotPath, content, previousSnapshotHash);
|
|
328
371
|
return {
|
|
329
372
|
...base,
|
|
330
373
|
strategy: "repository-metadata-only",
|
|
331
374
|
status: "metadata-only",
|
|
332
|
-
snapshotHash,
|
|
333
|
-
changed:
|
|
375
|
+
snapshotHash: written.snapshotHash,
|
|
376
|
+
changed: written.changed,
|
|
334
377
|
reason: "repository origin recorded as metadata; configure contract paths or checkout policy before broad acquisition",
|
|
335
378
|
validation: { repositoryNotCrawled: "pass", snapshotWritten: "pass" },
|
|
336
379
|
};
|
|
@@ -368,26 +411,29 @@ function blockedProviderManagedSnapshot(context) {
|
|
|
368
411
|
validation: { acquisitionMethodConfirmed: "fail", previousSnapshotPreserved: context.previousSnapshotHash ? "pass" : "skipped" },
|
|
369
412
|
};
|
|
370
413
|
}
|
|
371
|
-
async function runProviderAdapter(adapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
|
|
414
|
+
async function runProviderAdapter(adapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
|
|
372
415
|
if (adapter === "confluence") {
|
|
373
|
-
return runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
|
|
416
|
+
return runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
|
|
374
417
|
}
|
|
375
418
|
if (adapter === "jira") {
|
|
376
|
-
return runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
|
|
419
|
+
return runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
|
|
377
420
|
}
|
|
378
421
|
if (adapter === "gitlab") {
|
|
379
|
-
return runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
|
|
422
|
+
return runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
|
|
380
423
|
}
|
|
381
|
-
return runDesignAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts);
|
|
424
|
+
return runDesignAdapter(adapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot);
|
|
382
425
|
}
|
|
383
|
-
async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
|
|
426
|
+
async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
|
|
384
427
|
const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "confluence");
|
|
385
|
-
const
|
|
386
|
-
const
|
|
428
|
+
const exportWordUrl = firstOriginString(entry.source, "exportWordUrl", "wordExportUrl", "exportUrl");
|
|
429
|
+
const restUrl = firstOriginString(entry.source, "apiUrl", "restUrl") ?? deriveConfluenceRestUrl(entry.source);
|
|
430
|
+
const pageUrl = firstOriginString(entry.source, "url") ?? base.origin;
|
|
431
|
+
const url = exportWordUrl ?? restUrl ?? pageUrl;
|
|
432
|
+
const strategy = exportWordUrl ? "confluence-word-export" : "confluence-storage-rest";
|
|
387
433
|
if (!url)
|
|
388
|
-
return sourceGap(base, "confluence adapter needs origin.url, origin.apiUrl, or origin.
|
|
434
|
+
return sourceGap(base, "confluence adapter needs origin.url, origin.apiUrl, origin.restUrl, or origin.exportWordUrl");
|
|
389
435
|
if (!opts.allowNetwork) {
|
|
390
|
-
return blockedAdapter(base, strategy, "confluence source requires --allow-network and optional Playwright storageState before acquisition");
|
|
436
|
+
return blockedAdapter(base, strategy, "confluence source requires --allow-network and optional env auth or Playwright storageState before acquisition", cleanMetadata({ derivedUrl: restUrl }));
|
|
391
437
|
}
|
|
392
438
|
const fetched = await fetchAdapterBody(entry.source, url, authStatePath);
|
|
393
439
|
if (!fetched.ok)
|
|
@@ -396,15 +442,18 @@ async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapsho
|
|
|
396
442
|
return blockedAdapter(base, strategy, "confluence adapter resolved to login/chrome content; previous snapshot was preserved");
|
|
397
443
|
}
|
|
398
444
|
const parsed = parseJsonObject(fetched.body);
|
|
445
|
+
const wordExport = parseConfluenceWordExport(fetched.body, fetched.contentType);
|
|
399
446
|
const title = parsed ? stringValue(parsed, "title") : undefined;
|
|
400
447
|
const pageId = parsed ? firstStringValue(parsed, "id", "pageId", "contentId") : undefined;
|
|
401
448
|
const version = parsed ? versionValue(parsed) : undefined;
|
|
402
449
|
const storageHtml = parsed ? confluenceBodyValue(parsed) : undefined;
|
|
403
|
-
const content =
|
|
404
|
-
? htmlToStructuredMarkdown(
|
|
405
|
-
:
|
|
406
|
-
? htmlToStructuredMarkdown(
|
|
407
|
-
:
|
|
450
|
+
const content = wordExport?.html
|
|
451
|
+
? htmlToStructuredMarkdown(wordExport.html)
|
|
452
|
+
: storageHtml
|
|
453
|
+
? htmlToStructuredMarkdown(storageHtml)
|
|
454
|
+
: fetched.contentType.includes("html")
|
|
455
|
+
? htmlToStructuredMarkdown(fetched.body)
|
|
456
|
+
: fencedBody(fetched.body, fetched.contentType);
|
|
408
457
|
if (!hasMaterialContent(content)) {
|
|
409
458
|
return blockedAdapter(base, strategy, "confluence adapter produced no material body content; previous snapshot was preserved");
|
|
410
459
|
}
|
|
@@ -415,11 +464,11 @@ async function runConfluenceAdapter(target, entry, snapshotPath, previousSnapsho
|
|
|
415
464
|
contentType: fetched.contentType,
|
|
416
465
|
content,
|
|
417
466
|
reason: "confluence source fetched and normalized into structured Markdown",
|
|
418
|
-
metadata: cleanMetadata({ title, pageId, version, runtimeAuth: fetched.authSources.join(", ") || undefined }),
|
|
467
|
+
metadata: cleanMetadata({ title: wordExport?.title ?? title, pageId, version, runtimeAuth: fetched.authSources.join(", ") || undefined }),
|
|
419
468
|
validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
420
|
-
});
|
|
469
|
+
}, sharedSnapshot);
|
|
421
470
|
}
|
|
422
|
-
async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
|
|
471
|
+
async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
|
|
423
472
|
const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "jira");
|
|
424
473
|
const csvPath = firstOriginString(entry.source, ...JIRA_LOCAL_FILE_ORIGIN_KEYS);
|
|
425
474
|
if (csvPath) {
|
|
@@ -443,14 +492,15 @@ async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash,
|
|
|
443
492
|
reason: "jira CSV export imported and normalized into structured Markdown",
|
|
444
493
|
metadata: cleanMetadata({ issueCount: rows.length, fallback: "csv" }),
|
|
445
494
|
validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
446
|
-
});
|
|
495
|
+
}, sharedSnapshot);
|
|
447
496
|
}
|
|
448
|
-
const
|
|
497
|
+
const derivedUrl = deriveJiraRestUrl(entry.source);
|
|
498
|
+
const url = firstOriginString(entry.source, "apiUrl", "searchUrl") ?? derivedUrl ?? firstOriginString(entry.source, "url") ?? (base.origin?.startsWith("http") || base.origin?.startsWith("data:") ? base.origin : undefined);
|
|
449
499
|
if (!url)
|
|
450
500
|
return null;
|
|
451
501
|
const strategy = "jira-rest-search";
|
|
452
502
|
if (!opts.allowNetwork) {
|
|
453
|
-
return blockedAdapter(base, strategy, "jira REST source requires --allow-network and optional Playwright storageState before acquisition");
|
|
503
|
+
return blockedAdapter(base, strategy, "jira REST source requires --allow-network and optional env auth or Playwright storageState before acquisition", cleanMetadata({ derivedUrl }));
|
|
454
504
|
}
|
|
455
505
|
const fetched = await fetchAdapterBody(entry.source, url, authStatePath);
|
|
456
506
|
if (!fetched.ok)
|
|
@@ -472,9 +522,9 @@ async function runJiraAdapter(target, entry, snapshotPath, previousSnapshotHash,
|
|
|
472
522
|
reason: "jira REST source fetched and normalized into structured Markdown",
|
|
473
523
|
metadata: cleanMetadata({ issueCount: jiraIssueCount(parsed), runtimeAuth: fetched.authSources.join(", ") || undefined }),
|
|
474
524
|
validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
475
|
-
});
|
|
525
|
+
}, sharedSnapshot);
|
|
476
526
|
}
|
|
477
|
-
async function runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
|
|
527
|
+
async function runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
|
|
478
528
|
const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, "gitlab");
|
|
479
529
|
const url = firstOriginString(entry.source, "rawUrl", "apiUrl", "url");
|
|
480
530
|
if (!url)
|
|
@@ -512,10 +562,11 @@ async function runGitLabAdapter(target, entry, snapshotPath, previousSnapshotHas
|
|
|
512
562
|
runtimeAuth: fetched.authSources.join(", ") || undefined,
|
|
513
563
|
}),
|
|
514
564
|
validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
515
|
-
});
|
|
565
|
+
}, sharedSnapshot);
|
|
516
566
|
}
|
|
517
|
-
async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts) {
|
|
518
|
-
const
|
|
567
|
+
async function runDesignAdapter(adapter, target, entry, snapshotPath, previousSnapshotHash, authStatePath, opts, sharedSnapshot) {
|
|
568
|
+
const normalized = interpretSourceAdapter(entry.source);
|
|
569
|
+
const base = adapterBase(target, entry, snapshotPath, previousSnapshotHash, adapter);
|
|
519
570
|
const exportPath = firstOriginString(entry.source, ...DESIGN_LOCAL_FILE_ORIGIN_KEYS);
|
|
520
571
|
if (exportPath) {
|
|
521
572
|
const path = resolveAdapterLocalFilePath(target, exportPath);
|
|
@@ -529,7 +580,7 @@ async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHas
|
|
|
529
580
|
return blockedAdapter(base, "design-export-import", "design export produced no material content; previous snapshot was preserved");
|
|
530
581
|
}
|
|
531
582
|
return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
|
|
532
|
-
adapter
|
|
583
|
+
adapter,
|
|
533
584
|
strategy: "design-export-import",
|
|
534
585
|
resolvedOrigin: exportPath,
|
|
535
586
|
contentType: contentTypeForPath(exportPath),
|
|
@@ -537,21 +588,56 @@ async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHas
|
|
|
537
588
|
reason: "design export imported and normalized into structured Markdown",
|
|
538
589
|
metadata: cleanMetadata({ provider: base.provider, fallback: "export" }),
|
|
539
590
|
validation: { sourceExists: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
540
|
-
});
|
|
591
|
+
}, sharedSnapshot);
|
|
541
592
|
}
|
|
542
|
-
|
|
593
|
+
if (adapter === "design-source" && normalized.ambiguous && !exportPath) {
|
|
594
|
+
return sourceGap(base, "design source needs explicit adapter before provider-backed acquisition; configure source adapter, for example adapter: figma", designProviderGapMetadata("adapter-ambiguous", entry.source, undefined));
|
|
595
|
+
}
|
|
596
|
+
const figmaLocator = figmaLocatorForSource(entry.source);
|
|
597
|
+
const command = figmaCommandForSource(entry.source, target, relative(target, snapshotPath), figmaLocator);
|
|
598
|
+
if (command) {
|
|
599
|
+
if (!opts.allowNetwork) {
|
|
600
|
+
return blockedAdapter(base, "figma-mcp-command", "figma MCP command acquisition requires --allow-network because it may contact the local MCP bridge or Figma desktop plugin", designProviderGapMetadata("network-not-allowed", entry.source, figmaLocator));
|
|
601
|
+
}
|
|
602
|
+
const commandResult = await runDesignCommand(target, command);
|
|
603
|
+
if (!commandResult.ok) {
|
|
604
|
+
return blockedAdapter(base, "figma-mcp-command", commandResult.reason, designProviderGapMetadata("mcp-command-failed", entry.source, figmaLocator));
|
|
605
|
+
}
|
|
606
|
+
const content = renderDesignMarkdown(commandResult.body, commandResult.origin, commandResult.contentType);
|
|
607
|
+
if (!hasMaterialContent(content)) {
|
|
608
|
+
return blockedAdapter(base, "figma-mcp-command", "figma MCP command produced no material design content; previous snapshot was preserved", designProviderGapMetadata("mcp-command-empty", entry.source, figmaLocator));
|
|
609
|
+
}
|
|
610
|
+
return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
|
|
611
|
+
adapter,
|
|
612
|
+
strategy: "figma-mcp-command",
|
|
613
|
+
resolvedOrigin: commandResult.origin,
|
|
614
|
+
contentType: commandResult.contentType,
|
|
615
|
+
content,
|
|
616
|
+
reason: "design source captured through configured Figma MCP command and normalized into structured Markdown",
|
|
617
|
+
metadata: cleanMetadata({
|
|
618
|
+
provider: base.provider,
|
|
619
|
+
mcpProvider: command.provider,
|
|
620
|
+
command: command.safeLabel,
|
|
621
|
+
fileKey: figmaLocator?.fileKey,
|
|
622
|
+
nodeId: figmaLocator?.nodeId,
|
|
623
|
+
}),
|
|
624
|
+
validation: { commandExited: "pass", bodyContent: "pass", snapshotWritten: "pass", secretValuesHidden: "pass" },
|
|
625
|
+
}, sharedSnapshot);
|
|
626
|
+
}
|
|
627
|
+
const derivedApiUrl = figmaLocator?.apiUrl;
|
|
628
|
+
const url = firstOriginString(entry.source, "apiUrl", "exportUrl") ?? derivedApiUrl ?? firstOriginString(entry.source, "url") ?? base.origin;
|
|
543
629
|
if (!url) {
|
|
544
|
-
return sourceGap(base, "design source needs origin.url, origin.apiUrl, origin.exportUrl, or a local exportPath/assetPath/filePath before acquisition", designProviderGapMetadata("missing-provider-locator"));
|
|
630
|
+
return sourceGap(base, "design source needs origin.url, origin.apiUrl, origin.exportUrl, or a local exportPath/assetPath/filePath before acquisition", designProviderGapMetadata("missing-provider-locator", entry.source, figmaLocator));
|
|
545
631
|
}
|
|
546
632
|
if (!opts.allowNetwork) {
|
|
547
|
-
return blockedAdapter(base, opts.browser ? "design-browser-render" : "design-api-or-browser", "design source requires --allow-network with token/export URL or --browser storageState acquisition", designProviderGapMetadata("network-not-allowed"));
|
|
633
|
+
return blockedAdapter(base, opts.browser ? "design-browser-render" : "design-api-or-browser", "design source requires --allow-network with token/export URL or --browser storageState acquisition", designProviderGapMetadata("network-not-allowed", entry.source, figmaLocator));
|
|
548
634
|
}
|
|
549
635
|
const directRuntimeAuth = await hasDirectRuntimeAuthForSource(entry.source, url, authStatePath);
|
|
550
636
|
if (opts.browser && !directRuntimeAuth) {
|
|
551
637
|
const rendered = await fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
|
|
552
638
|
return {
|
|
553
639
|
...rendered,
|
|
554
|
-
adapter
|
|
640
|
+
adapter,
|
|
555
641
|
metadata: cleanMetadata({ provider: base.provider, acquisition: "browser" }),
|
|
556
642
|
};
|
|
557
643
|
}
|
|
@@ -559,29 +645,35 @@ async function runDesignAdapter(target, entry, snapshotPath, previousSnapshotHas
|
|
|
559
645
|
if (!fetched.ok) {
|
|
560
646
|
if (opts.browser)
|
|
561
647
|
return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
|
|
562
|
-
return blockedAdapter(base, "design-api-or-export", fetched.reason, designProviderGapMetadata("provider-fetch-failed"));
|
|
648
|
+
return blockedAdapter(base, "design-api-or-export", fetched.reason, designProviderGapMetadata("provider-fetch-failed", entry.source, figmaLocator));
|
|
563
649
|
}
|
|
564
650
|
if (looksLikeLoginPage(fetched.body, fetched.resolvedOrigin)) {
|
|
565
651
|
if (opts.browser)
|
|
566
652
|
return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
|
|
567
|
-
return blockedAdapter(base, "design-api-or-export", "design adapter resolved to login/chrome content; previous snapshot was preserved", designProviderGapMetadata("provider-auth-blocked"));
|
|
653
|
+
return blockedAdapter(base, "design-api-or-export", "design adapter resolved to login/chrome content; previous snapshot was preserved", designProviderGapMetadata("provider-auth-blocked", entry.source, figmaLocator));
|
|
568
654
|
}
|
|
569
655
|
const content = renderDesignMarkdown(fetched.body, fetched.resolvedOrigin, fetched.contentType);
|
|
570
656
|
if (!hasMaterialContent(content)) {
|
|
571
657
|
if (opts.browser)
|
|
572
658
|
return fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previousSnapshotHash, authStatePath);
|
|
573
|
-
return blockedAdapter(base, "design-api-or-export", "design adapter produced no material design content; previous snapshot was preserved", designProviderGapMetadata("provider-content-incomplete"));
|
|
659
|
+
return blockedAdapter(base, "design-api-or-export", "design adapter produced no material design content; previous snapshot was preserved", designProviderGapMetadata("provider-content-incomplete", entry.source, figmaLocator));
|
|
574
660
|
}
|
|
575
661
|
return writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, {
|
|
576
|
-
adapter
|
|
662
|
+
adapter,
|
|
577
663
|
strategy: "design-api-or-export",
|
|
578
664
|
resolvedOrigin: fetched.resolvedOrigin,
|
|
579
665
|
contentType: fetched.contentType,
|
|
580
666
|
content,
|
|
581
667
|
reason: "design source fetched and normalized into structured Markdown",
|
|
582
|
-
metadata: cleanMetadata({
|
|
668
|
+
metadata: cleanMetadata({
|
|
669
|
+
provider: base.provider,
|
|
670
|
+
runtimeAuth: fetched.authSources.join(", ") || undefined,
|
|
671
|
+
derivedApiUrl,
|
|
672
|
+
fileKey: figmaLocator?.fileKey,
|
|
673
|
+
nodeId: figmaLocator?.nodeId,
|
|
674
|
+
}),
|
|
583
675
|
validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
584
|
-
});
|
|
676
|
+
}, sharedSnapshot);
|
|
585
677
|
}
|
|
586
678
|
function adapterBase(target, entry, snapshotPath, previousSnapshotHash, adapter) {
|
|
587
679
|
const origin = inferSourceOrigin(entry.source);
|
|
@@ -597,12 +689,10 @@ function adapterBase(target, entry, snapshotPath, previousSnapshotHash, adapter)
|
|
|
597
689
|
previousSnapshotHash,
|
|
598
690
|
};
|
|
599
691
|
}
|
|
600
|
-
async function writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, result) {
|
|
692
|
+
async function writeAdapterSnapshot(target, entry, snapshotPath, previousSnapshotHash, result, sharedSnapshot) {
|
|
601
693
|
const origin = inferSourceOrigin(entry.source);
|
|
602
|
-
const markdown = renderAdapterMarkdown(entry, result.resolvedOrigin, result.contentType, result.content, result.strategy, result.adapter, result.metadata);
|
|
603
|
-
await
|
|
604
|
-
await writeFile(snapshotPath, markdown, "utf8");
|
|
605
|
-
const snapshotHash = await sha256(snapshotPath);
|
|
694
|
+
const markdown = renderAdapterMarkdown(entry, result.resolvedOrigin, result.contentType, result.content, result.strategy, result.adapter, result.metadata, sharedSnapshot);
|
|
695
|
+
const written = await writePreparedSnapshot(target, snapshotPath, markdown, previousSnapshotHash);
|
|
606
696
|
return {
|
|
607
697
|
envelopeVersion: 1,
|
|
608
698
|
sourceKey: entry.id,
|
|
@@ -615,11 +705,11 @@ async function writeAdapterSnapshot(target, entry, snapshotPath, previousSnapsho
|
|
|
615
705
|
origin: origin.ref,
|
|
616
706
|
resolvedOrigin: result.resolvedOrigin,
|
|
617
707
|
contentType: result.contentType,
|
|
618
|
-
snapshot:
|
|
619
|
-
snapshotHash,
|
|
708
|
+
snapshot: written.snapshot,
|
|
709
|
+
snapshotHash: written.snapshotHash,
|
|
620
710
|
previousSnapshotHash,
|
|
621
711
|
metadata: result.metadata,
|
|
622
|
-
changed:
|
|
712
|
+
changed: written.changed,
|
|
623
713
|
reason: result.reason,
|
|
624
714
|
validation: result.validation,
|
|
625
715
|
};
|
|
@@ -643,13 +733,18 @@ async function headersForSource(source, url, authStatePath) {
|
|
|
643
733
|
const authSources = [];
|
|
644
734
|
const token = envValue(authStringList(source, "tokenEnv", "accessTokenEnv", "apiTokenEnv"));
|
|
645
735
|
if (token.value) {
|
|
646
|
-
const headerName = firstAuthString(source, "tokenHeader", "headerName") ??
|
|
647
|
-
const tokenScheme = firstAuthString(source, "tokenScheme", "scheme") ??
|
|
736
|
+
const headerName = firstAuthString(source, "tokenHeader", "headerName") ?? defaultTokenHeader(source);
|
|
737
|
+
const tokenScheme = firstAuthString(source, "tokenScheme", "scheme") ?? defaultTokenScheme(source, headerName);
|
|
648
738
|
headers[headerName] = headerName.toLowerCase() === "authorization" && tokenScheme
|
|
649
739
|
? `${tokenScheme} ${token.value}`
|
|
650
740
|
: token.value;
|
|
651
741
|
authSources.push(`env:${token.name}`);
|
|
652
742
|
}
|
|
743
|
+
const basic = basicAuthHeaderFromEnvironment(source);
|
|
744
|
+
if (!headers.authorization && basic.value) {
|
|
745
|
+
headers.authorization = basic.value;
|
|
746
|
+
authSources.push(...basic.sources.map((name) => `env:${name}`));
|
|
747
|
+
}
|
|
653
748
|
const envCookie = cookieHeaderFromEnvironment(source);
|
|
654
749
|
if (envCookie.source)
|
|
655
750
|
authSources.push(`env:${envCookie.source}`);
|
|
@@ -663,7 +758,10 @@ async function headersForSource(source, url, authStatePath) {
|
|
|
663
758
|
}
|
|
664
759
|
async function hasDirectRuntimeAuthForSource(source, url, authStatePath) {
|
|
665
760
|
const { headers } = await headersForSource(source, url, authStatePath);
|
|
666
|
-
return
|
|
761
|
+
return hasRuntimeAuthHeaders(headers);
|
|
762
|
+
}
|
|
763
|
+
function hasRuntimeAuthHeaders(headers) {
|
|
764
|
+
return Object.keys(headers).length > 0;
|
|
667
765
|
}
|
|
668
766
|
async function fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHash, authStatePath) {
|
|
669
767
|
const origin = inferSourceOrigin(entry.source);
|
|
@@ -698,17 +796,15 @@ async function fetchUrlSnapshot(target, entry, snapshotPath, previousSnapshotHas
|
|
|
698
796
|
return blocked(base, "remote fetch produced no material body content; previous snapshot was preserved");
|
|
699
797
|
}
|
|
700
798
|
const markdown = renderRemoteMarkdown(entry, response.url, contentType, content);
|
|
701
|
-
await
|
|
702
|
-
await writeFile(snapshotPath, markdown, "utf8");
|
|
703
|
-
const snapshotHash = await sha256(snapshotPath);
|
|
799
|
+
const written = await writePreparedSnapshot(target, snapshotPath, markdown, previousSnapshotHash);
|
|
704
800
|
return {
|
|
705
801
|
...base,
|
|
706
802
|
strategy: authSources.length > 0 ? "runtime-auth-direct-fetch" : "direct-fetch-structured-markdown",
|
|
707
803
|
status: "materialized",
|
|
708
804
|
resolvedOrigin: response.url,
|
|
709
805
|
contentType,
|
|
710
|
-
snapshotHash,
|
|
711
|
-
changed:
|
|
806
|
+
snapshotHash: written.snapshotHash,
|
|
807
|
+
changed: written.changed,
|
|
712
808
|
reason: "remote URL fetched and normalized into structured Markdown",
|
|
713
809
|
metadata: cleanMetadata({ runtimeAuth: authSources.join(", ") || undefined }),
|
|
714
810
|
validation: { httpOk: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
@@ -753,17 +849,15 @@ async function fetchUrlSnapshotWithBrowser(target, entry, snapshotPath, previous
|
|
|
753
849
|
return blocked(base, "browser acquisition produced no material body content; previous snapshot was preserved");
|
|
754
850
|
}
|
|
755
851
|
const markdown = renderRemoteMarkdown(entry, resolvedUrl, "text/html; rendered=playwright", content, "playwright-headless-browser");
|
|
756
|
-
await
|
|
757
|
-
await writeFile(snapshotPath, markdown, "utf8");
|
|
758
|
-
const snapshotHash = await sha256(snapshotPath);
|
|
852
|
+
const written = await writePreparedSnapshot(target, snapshotPath, markdown, previousSnapshotHash);
|
|
759
853
|
return {
|
|
760
854
|
...base,
|
|
761
855
|
strategy: "playwright-headless-browser",
|
|
762
856
|
status: "materialized",
|
|
763
857
|
resolvedOrigin: resolvedUrl,
|
|
764
858
|
contentType: "text/html; rendered=playwright",
|
|
765
|
-
snapshotHash,
|
|
766
|
-
changed:
|
|
859
|
+
snapshotHash: written.snapshotHash,
|
|
860
|
+
changed: written.changed,
|
|
767
861
|
reason: "remote URL rendered through Playwright and normalized into structured Markdown",
|
|
768
862
|
validation: { browserRendered: "pass", loginPageRejected: "pass", bodyContent: "pass", snapshotWritten: "pass" },
|
|
769
863
|
};
|
|
@@ -807,6 +901,63 @@ async function renderStructuredLocalMarkdown(target, entry, originPath) {
|
|
|
807
901
|
"",
|
|
808
902
|
].filter((line) => typeof line === "string").join("\n");
|
|
809
903
|
}
|
|
904
|
+
async function renderStructuredLocalDirectoryMarkdown(target, entry, originPath) {
|
|
905
|
+
const entries = await readdir(originPath, { withFileTypes: true }).catch(() => []);
|
|
906
|
+
const git = await readGitDirectoryMetadata(originPath);
|
|
907
|
+
return [
|
|
908
|
+
"---",
|
|
909
|
+
`sourceKey: ${JSON.stringify(entry.id)}`,
|
|
910
|
+
`sourcePath: ${JSON.stringify(entry.path)}`,
|
|
911
|
+
entry.provider !== entry.id ? `sourceAlias: ${JSON.stringify(entry.provider)}` : undefined,
|
|
912
|
+
`origin: ${JSON.stringify(relative(target, originPath))}`,
|
|
913
|
+
`extractionMethod: "local-directory-summary"`,
|
|
914
|
+
`extractedAt: ${JSON.stringify(new Date().toISOString())}`,
|
|
915
|
+
`snapshotStatus: "metadata-only"`,
|
|
916
|
+
git.head ? `gitHead: ${JSON.stringify(git.head)}` : undefined,
|
|
917
|
+
git.status ? `gitStatus: ${JSON.stringify(git.status)}` : undefined,
|
|
918
|
+
"---",
|
|
919
|
+
"",
|
|
920
|
+
"# Directory Resource Snapshot",
|
|
921
|
+
"",
|
|
922
|
+
"## Provenance",
|
|
923
|
+
"",
|
|
924
|
+
`- Source key: \`${entry.id}\``,
|
|
925
|
+
entry.provider !== entry.id ? `- Source alias: \`${entry.provider}\`` : undefined,
|
|
926
|
+
`- Origin: \`${relative(target, originPath)}\``,
|
|
927
|
+
"- Method: local-directory-summary",
|
|
928
|
+
git.head ? `- Git HEAD: \`${git.head}\`` : undefined,
|
|
929
|
+
git.status ? `- Git status: ${git.status}` : undefined,
|
|
930
|
+
"",
|
|
931
|
+
"## Top-Level Entries",
|
|
932
|
+
"",
|
|
933
|
+
entries.length === 0
|
|
934
|
+
? "- Empty directory."
|
|
935
|
+
: entries
|
|
936
|
+
.slice(0, 200)
|
|
937
|
+
.map((entry) => `- ${entry.isDirectory() ? "dir" : "file"}: \`${entry.name}\``)
|
|
938
|
+
.join("\n"),
|
|
939
|
+
entries.length > 200 ? `- ... ${entries.length - 200} more entries omitted.` : undefined,
|
|
940
|
+
"",
|
|
941
|
+
].filter((line) => typeof line === "string").join("\n");
|
|
942
|
+
}
|
|
943
|
+
async function readGitDirectoryMetadata(originPath) {
|
|
944
|
+
const head = await gitOutput(originPath, ["rev-parse", "HEAD"]);
|
|
945
|
+
const status = await gitOutput(originPath, ["status", "--short"]);
|
|
946
|
+
return {
|
|
947
|
+
head,
|
|
948
|
+
status: status ? "dirty" : head ? "clean" : undefined,
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
async function gitOutput(cwd, args) {
|
|
952
|
+
try {
|
|
953
|
+
const { stdout } = await execFileAsync("git", args, { cwd, timeout: 10_000, maxBuffer: 1024 * 1024 });
|
|
954
|
+
const value = stdout.trim();
|
|
955
|
+
return value || undefined;
|
|
956
|
+
}
|
|
957
|
+
catch {
|
|
958
|
+
return undefined;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
810
961
|
function renderRemoteMarkdown(entry, resolvedUrl, contentType, content, extractionMethod = "direct-fetch") {
|
|
811
962
|
return [
|
|
812
963
|
"---",
|
|
@@ -835,7 +986,33 @@ function renderRemoteMarkdown(entry, resolvedUrl, contentType, content, extracti
|
|
|
835
986
|
"",
|
|
836
987
|
].filter((line) => typeof line === "string").join("\n");
|
|
837
988
|
}
|
|
838
|
-
function
|
|
989
|
+
function sharedSnapshotProvenance(target, config, entry, entries) {
|
|
990
|
+
const snapshotPath = resolve(target, resolveSourceSnapshot(config, entry));
|
|
991
|
+
const shared = entries.filter((candidate) => resolve(target, resolveSourceSnapshot(config, candidate)) === snapshotPath);
|
|
992
|
+
if (shared.length <= 1)
|
|
993
|
+
return undefined;
|
|
994
|
+
return {
|
|
995
|
+
sourceKeys: shared.map((candidate) => candidate.id),
|
|
996
|
+
sourcePaths: shared.map((candidate) => candidate.path),
|
|
997
|
+
sourceAliases: uniqueStrings(shared.map((candidate) => candidate.provider).filter(Boolean)),
|
|
998
|
+
sourceTypes: uniqueStrings(shared.map(sourceType).filter((value) => Boolean(value))),
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
function sourceType(entry) {
|
|
1002
|
+
return typeof entry.source.type === "string" && entry.source.type.trim() ? entry.source.type : undefined;
|
|
1003
|
+
}
|
|
1004
|
+
function uniqueStrings(values) {
|
|
1005
|
+
return Array.from(new Set(values));
|
|
1006
|
+
}
|
|
1007
|
+
function yamlStringList(key, values) {
|
|
1008
|
+
if (values.length === 0)
|
|
1009
|
+
return [];
|
|
1010
|
+
return [
|
|
1011
|
+
`${key}:`,
|
|
1012
|
+
...values.map((value) => ` - ${JSON.stringify(value)}`),
|
|
1013
|
+
];
|
|
1014
|
+
}
|
|
1015
|
+
function renderAdapterMarkdown(entry, resolvedOrigin, contentType, content, extractionMethod, adapter, metadata, sharedSnapshot) {
|
|
839
1016
|
const metadataLines = metadata && Object.keys(metadata).length > 0
|
|
840
1017
|
? [
|
|
841
1018
|
"## Adapter Metadata",
|
|
@@ -844,11 +1021,28 @@ function renderAdapterMarkdown(entry, resolvedOrigin, contentType, content, extr
|
|
|
844
1021
|
"",
|
|
845
1022
|
]
|
|
846
1023
|
: [];
|
|
1024
|
+
const sharedFrontmatter = sharedSnapshot
|
|
1025
|
+
? [
|
|
1026
|
+
`snapshotProvenance: "shared-config-snapshot"`,
|
|
1027
|
+
...yamlStringList("sourceKeys", sharedSnapshot.sourceKeys),
|
|
1028
|
+
...yamlStringList("sourcePaths", sharedSnapshot.sourcePaths),
|
|
1029
|
+
...yamlStringList("sourceAliases", sharedSnapshot.sourceAliases),
|
|
1030
|
+
...yamlStringList("sourceTypes", sharedSnapshot.sourceTypes),
|
|
1031
|
+
]
|
|
1032
|
+
: [];
|
|
1033
|
+
const sharedProvenance = sharedSnapshot
|
|
1034
|
+
? [
|
|
1035
|
+
`- Shared snapshot: ${sharedSnapshot.sourceKeys.length} configured sources`,
|
|
1036
|
+
`- Source keys: ${sharedSnapshot.sourceKeys.map((key) => `\`${key}\``).join(", ")}`,
|
|
1037
|
+
`- Source aliases: ${sharedSnapshot.sourceAliases.map((alias) => `\`${alias}\``).join(", ")}`,
|
|
1038
|
+
]
|
|
1039
|
+
: [];
|
|
847
1040
|
return [
|
|
848
1041
|
"---",
|
|
849
1042
|
`sourceKey: ${JSON.stringify(entry.id)}`,
|
|
850
1043
|
`sourcePath: ${JSON.stringify(entry.path)}`,
|
|
851
1044
|
entry.provider !== entry.id ? `sourceAlias: ${JSON.stringify(entry.provider)}` : undefined,
|
|
1045
|
+
...sharedFrontmatter,
|
|
852
1046
|
`origin: ${JSON.stringify(resolvedOrigin)}`,
|
|
853
1047
|
`contentType: ${JSON.stringify(contentType)}`,
|
|
854
1048
|
`adapter: ${JSON.stringify(adapter)}`,
|
|
@@ -863,6 +1057,7 @@ function renderAdapterMarkdown(entry, resolvedOrigin, contentType, content, extr
|
|
|
863
1057
|
"",
|
|
864
1058
|
`- Source key: \`${entry.id}\``,
|
|
865
1059
|
entry.provider !== entry.id ? `- Source alias: \`${entry.provider}\`` : undefined,
|
|
1060
|
+
...sharedProvenance,
|
|
866
1061
|
`- Resolved origin: \`${resolvedOrigin}\``,
|
|
867
1062
|
`- Adapter: ${adapter}`,
|
|
868
1063
|
`- Method: ${extractionMethod}`,
|
|
@@ -883,6 +1078,75 @@ function confluenceBodyValue(value) {
|
|
|
883
1078
|
const view = recordValue(body, "view");
|
|
884
1079
|
return stringValue(storage, "value") ?? stringValue(view, "value") ?? stringValue(value, "content") ?? stringValue(value, "value");
|
|
885
1080
|
}
|
|
1081
|
+
function parseConfluenceWordExport(body, contentType) {
|
|
1082
|
+
if (!/multipart\/related|application\/vnd\.ms-word|MIME-Version:/i.test(`${contentType}\n${body.slice(0, 512)}`)) {
|
|
1083
|
+
return undefined;
|
|
1084
|
+
}
|
|
1085
|
+
const rootHeadersEnd = body.search(/\r?\n\r?\n/);
|
|
1086
|
+
if (rootHeadersEnd === -1)
|
|
1087
|
+
return undefined;
|
|
1088
|
+
const rootHeaders = parseMimeHeaders(body.slice(0, rootHeadersEnd));
|
|
1089
|
+
const boundary = /boundary="?([^";\r\n]+)"?/i.exec(rootHeaders["content-type"] ?? "")?.[1];
|
|
1090
|
+
if (!boundary)
|
|
1091
|
+
return undefined;
|
|
1092
|
+
const chunks = body.split(`--${boundary}`).slice(1);
|
|
1093
|
+
for (const chunk of chunks) {
|
|
1094
|
+
if (chunk.startsWith("--"))
|
|
1095
|
+
continue;
|
|
1096
|
+
const trimmed = chunk.replace(/^\r?\n/, "");
|
|
1097
|
+
const headerEnd = trimmed.search(/\r?\n\r?\n/);
|
|
1098
|
+
if (headerEnd === -1)
|
|
1099
|
+
continue;
|
|
1100
|
+
const headers = parseMimeHeaders(trimmed.slice(0, headerEnd));
|
|
1101
|
+
if (!/text\/html/i.test(headers["content-type"] ?? ""))
|
|
1102
|
+
continue;
|
|
1103
|
+
const html = decodeMimePartBody(trimmed.slice(headerEnd).replace(/^\r?\n\r?\n?/, ""), headers);
|
|
1104
|
+
return {
|
|
1105
|
+
html,
|
|
1106
|
+
title: decodeHtmlEntities(stripHtml(html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1] ?? "")) || undefined,
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
return undefined;
|
|
1110
|
+
}
|
|
1111
|
+
function parseMimeHeaders(text) {
|
|
1112
|
+
const headers = {};
|
|
1113
|
+
let current;
|
|
1114
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1115
|
+
if (/^\s/.test(line) && current) {
|
|
1116
|
+
headers[current] += ` ${line.trim()}`;
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
const index = line.indexOf(":");
|
|
1120
|
+
if (index === -1)
|
|
1121
|
+
continue;
|
|
1122
|
+
current = line.slice(0, index).toLowerCase();
|
|
1123
|
+
headers[current] = line.slice(index + 1).trim();
|
|
1124
|
+
}
|
|
1125
|
+
return headers;
|
|
1126
|
+
}
|
|
1127
|
+
function decodeMimePartBody(body, headers) {
|
|
1128
|
+
const encoding = (headers["content-transfer-encoding"] ?? "").toLowerCase();
|
|
1129
|
+
if (encoding.includes("quoted-printable"))
|
|
1130
|
+
return decodeQuotedPrintable(body);
|
|
1131
|
+
if (encoding.includes("base64"))
|
|
1132
|
+
return Buffer.from(body.replace(/\s+/g, ""), "base64").toString("utf8");
|
|
1133
|
+
return body;
|
|
1134
|
+
}
|
|
1135
|
+
function decodeQuotedPrintable(input) {
|
|
1136
|
+
const normalized = input.replace(/=\r?\n/g, "");
|
|
1137
|
+
const bytes = [];
|
|
1138
|
+
for (let i = 0; i < normalized.length; i += 1) {
|
|
1139
|
+
if (normalized[i] === "=" && /^[0-9a-fA-F]{2}$/.test(normalized.slice(i + 1, i + 3))) {
|
|
1140
|
+
bytes.push(parseInt(normalized.slice(i + 1, i + 3), 16));
|
|
1141
|
+
i += 2;
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
const char = normalized[i];
|
|
1145
|
+
if (char)
|
|
1146
|
+
bytes.push(...Buffer.from(char, "utf8"));
|
|
1147
|
+
}
|
|
1148
|
+
return Buffer.from(bytes).toString("utf8");
|
|
1149
|
+
}
|
|
886
1150
|
function versionValue(value) {
|
|
887
1151
|
const version = recordValue(value, "version");
|
|
888
1152
|
const number = version?.number;
|
|
@@ -1021,13 +1285,230 @@ function designNodeNames(value) {
|
|
|
1021
1285
|
const nested = Array.isArray(children) ? children.flatMap(designNodeNames) : [];
|
|
1022
1286
|
return current ? [current, ...nested] : nested;
|
|
1023
1287
|
}
|
|
1024
|
-
function designProviderGapMetadata(reason) {
|
|
1288
|
+
function designProviderGapMetadata(reason, source, figma) {
|
|
1289
|
+
const mcpProvider = source ? interpretSourceAdapter(source).mcpProviderName : undefined;
|
|
1025
1290
|
return {
|
|
1026
1291
|
providerGap: reason,
|
|
1027
1292
|
fallbackEvidenceRequired: true,
|
|
1293
|
+
mcpProvider,
|
|
1294
|
+
derivedApiUrl: figma?.apiUrl,
|
|
1295
|
+
fileKey: figma?.fileKey,
|
|
1296
|
+
nodeId: figma?.nodeId,
|
|
1297
|
+
nextActions: [
|
|
1298
|
+
"configure a local exportPath/assetPath/filePath",
|
|
1299
|
+
"configure origin.mcpCommand + origin.mcpArgs for a Figma MCP capture command",
|
|
1300
|
+
"use Playwright/browser acquisition with --browser when HTML rendering is sufficient",
|
|
1301
|
+
"capture Computer Use or human visual evidence outside the CLI and attach it as a raw source",
|
|
1302
|
+
].join("; "),
|
|
1028
1303
|
reconstructionRisk: "semantic UE reconstruction needs provider raw payload, assets/raw files, reconstruction.json, and known gaps before downstream design use",
|
|
1029
1304
|
};
|
|
1030
1305
|
}
|
|
1306
|
+
function deriveConfluenceRestUrl(source) {
|
|
1307
|
+
const pageId = firstOriginString(source, "pageId", "contentId", "id") ?? confluencePageIdFromUrl(firstOriginString(source, "url"));
|
|
1308
|
+
if (!pageId)
|
|
1309
|
+
return undefined;
|
|
1310
|
+
const base = confluenceBaseUrl(source);
|
|
1311
|
+
if (!base)
|
|
1312
|
+
return undefined;
|
|
1313
|
+
return `${base}/rest/api/content/${encodeURIComponent(pageId)}?expand=body.storage,version`;
|
|
1314
|
+
}
|
|
1315
|
+
function confluencePageIdFromUrl(value) {
|
|
1316
|
+
if (!value)
|
|
1317
|
+
return undefined;
|
|
1318
|
+
try {
|
|
1319
|
+
const url = new URL(value);
|
|
1320
|
+
return /\/pages\/(\d+)/.exec(url.pathname)?.[1] ?? undefined;
|
|
1321
|
+
}
|
|
1322
|
+
catch {
|
|
1323
|
+
return /\/pages\/(\d+)/.exec(value)?.[1] ?? undefined;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
function confluenceBaseUrl(source) {
|
|
1327
|
+
const explicit = firstOriginString(source, "baseUrl", "siteUrl", "host");
|
|
1328
|
+
if (explicit)
|
|
1329
|
+
return trimTrailingSlash(explicit);
|
|
1330
|
+
const pageUrl = firstOriginString(source, "url");
|
|
1331
|
+
if (!pageUrl)
|
|
1332
|
+
return undefined;
|
|
1333
|
+
try {
|
|
1334
|
+
const url = new URL(pageUrl);
|
|
1335
|
+
const wikiPrefix = url.pathname.startsWith("/wiki/") ? "/wiki" : "";
|
|
1336
|
+
return `${url.protocol}//${url.host}${wikiPrefix}`;
|
|
1337
|
+
}
|
|
1338
|
+
catch {
|
|
1339
|
+
return undefined;
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
function deriveJiraRestUrl(source) {
|
|
1343
|
+
const explicitIssue = firstOriginString(source, "issueKey", "key", "id") ?? jiraIssueKeyFromUrl(firstOriginString(source, "url"));
|
|
1344
|
+
const query = firstOriginString(source, "jql", "query");
|
|
1345
|
+
const base = jiraBaseUrl(source);
|
|
1346
|
+
if (!base)
|
|
1347
|
+
return undefined;
|
|
1348
|
+
if (explicitIssue)
|
|
1349
|
+
return `${base}/rest/api/3/issue/${encodeURIComponent(explicitIssue)}`;
|
|
1350
|
+
if (query)
|
|
1351
|
+
return `${base}/rest/api/3/search?jql=${encodeURIComponent(query)}`;
|
|
1352
|
+
return undefined;
|
|
1353
|
+
}
|
|
1354
|
+
function jiraIssueKeyFromUrl(value) {
|
|
1355
|
+
if (!value)
|
|
1356
|
+
return undefined;
|
|
1357
|
+
try {
|
|
1358
|
+
const url = new URL(value);
|
|
1359
|
+
return /\/browse\/([A-Z][A-Z0-9_]+-\d+)/i.exec(url.pathname)?.[1] ?? undefined;
|
|
1360
|
+
}
|
|
1361
|
+
catch {
|
|
1362
|
+
return /\/browse\/([A-Z][A-Z0-9_]+-\d+)/i.exec(value)?.[1] ?? undefined;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
function jiraBaseUrl(source) {
|
|
1366
|
+
const explicit = firstOriginString(source, "baseUrl", "siteUrl", "host");
|
|
1367
|
+
if (explicit)
|
|
1368
|
+
return trimTrailingSlash(explicit);
|
|
1369
|
+
const urlValue = firstOriginString(source, "url");
|
|
1370
|
+
if (!urlValue)
|
|
1371
|
+
return undefined;
|
|
1372
|
+
try {
|
|
1373
|
+
const url = new URL(urlValue);
|
|
1374
|
+
return `${url.protocol}//${url.host}`;
|
|
1375
|
+
}
|
|
1376
|
+
catch {
|
|
1377
|
+
return undefined;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
function figmaLocatorForSource(source) {
|
|
1381
|
+
if (!isFigmaSource(source))
|
|
1382
|
+
return undefined;
|
|
1383
|
+
const sourceUrl = firstOriginString(source, "url");
|
|
1384
|
+
const fileKey = firstOriginString(source, "fileKey", "file_key", "fileId", "file_id", "id") ?? figmaFileKeyFromUrl(sourceUrl);
|
|
1385
|
+
const nodeId = normalizeFigmaNodeId(firstOriginString(source, "nodeId", "node_id") ?? figmaNodeIdFromUrl(sourceUrl));
|
|
1386
|
+
const explicitApiUrl = firstOriginString(source, "apiUrl", "exportUrl");
|
|
1387
|
+
const apiUrl = explicitApiUrl ?? (fileKey ? figmaApiUrl(fileKey, nodeId) : undefined);
|
|
1388
|
+
return cleanMetadata({ sourceUrl, fileKey, nodeId, apiUrl });
|
|
1389
|
+
}
|
|
1390
|
+
function isFigmaSource(source) {
|
|
1391
|
+
return [
|
|
1392
|
+
source.origin?.kind,
|
|
1393
|
+
source.origin?.provider,
|
|
1394
|
+
source.kind,
|
|
1395
|
+
source.type,
|
|
1396
|
+
firstOriginString(source, "mcpProvider", "mcpServer"),
|
|
1397
|
+
firstOriginString(source, "url"),
|
|
1398
|
+
]
|
|
1399
|
+
.filter((value) => typeof value === "string")
|
|
1400
|
+
.some((value) => value.toLowerCase().includes("figma"));
|
|
1401
|
+
}
|
|
1402
|
+
function figmaFileKeyFromUrl(value) {
|
|
1403
|
+
if (!value)
|
|
1404
|
+
return undefined;
|
|
1405
|
+
try {
|
|
1406
|
+
const url = new URL(value);
|
|
1407
|
+
return /\/(?:file|design)\/([^/?#]+)/i.exec(url.pathname)?.[1] ?? undefined;
|
|
1408
|
+
}
|
|
1409
|
+
catch {
|
|
1410
|
+
return /\/(?:file|design)\/([^/?#]+)/i.exec(value)?.[1] ?? undefined;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
function figmaNodeIdFromUrl(value) {
|
|
1414
|
+
if (!value)
|
|
1415
|
+
return undefined;
|
|
1416
|
+
try {
|
|
1417
|
+
return new URL(value).searchParams.get("node-id") ?? undefined;
|
|
1418
|
+
}
|
|
1419
|
+
catch {
|
|
1420
|
+
return /[?&]node-id=([^&#]+)/i.exec(value)?.[1] ?? undefined;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
function normalizeFigmaNodeId(value) {
|
|
1424
|
+
if (!value)
|
|
1425
|
+
return undefined;
|
|
1426
|
+
const decoded = decodeURIComponent(value.trim());
|
|
1427
|
+
return decoded.includes(":") ? decoded : decoded.replace("-", ":");
|
|
1428
|
+
}
|
|
1429
|
+
function figmaApiUrl(fileKey, nodeId) {
|
|
1430
|
+
const encodedFileKey = encodeURIComponent(fileKey);
|
|
1431
|
+
if (nodeId)
|
|
1432
|
+
return `https://api.figma.com/v1/files/${encodedFileKey}/nodes?ids=${encodeURIComponent(nodeId)}`;
|
|
1433
|
+
return `https://api.figma.com/v1/files/${encodedFileKey}`;
|
|
1434
|
+
}
|
|
1435
|
+
function figmaCommandForSource(source, target, snapshot, figma) {
|
|
1436
|
+
const command = firstOriginString(source, "mcpCommand", "command", "exportCommand");
|
|
1437
|
+
if (!command)
|
|
1438
|
+
return undefined;
|
|
1439
|
+
const args = originStringList(source, "mcpArgs", "args", "commandArgs")
|
|
1440
|
+
.map((arg) => renderCommandTemplate(arg, source, snapshot, figma));
|
|
1441
|
+
const provider = interpretSourceAdapter(source).mcpProviderName;
|
|
1442
|
+
const contentType = firstOriginString(source, "commandContentType", "contentType") ?? "application/json";
|
|
1443
|
+
const timeoutMs = numberOriginValue(source, "timeoutMs") ?? DEFAULT_ADAPTER_COMMAND_TIMEOUT_MS;
|
|
1444
|
+
const resolvedCommand = resolveCommandForTarget(target, command);
|
|
1445
|
+
return {
|
|
1446
|
+
command: resolvedCommand,
|
|
1447
|
+
args,
|
|
1448
|
+
provider,
|
|
1449
|
+
safeLabel: `${command} (${args.length} args)`,
|
|
1450
|
+
contentType,
|
|
1451
|
+
timeoutMs,
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
async function runDesignCommand(target, command) {
|
|
1455
|
+
try {
|
|
1456
|
+
const { stdout } = await execFileAsync(command.command, command.args, {
|
|
1457
|
+
cwd: target,
|
|
1458
|
+
env: process.env,
|
|
1459
|
+
timeout: command.timeoutMs,
|
|
1460
|
+
maxBuffer: DEFAULT_ADAPTER_COMMAND_MAX_BUFFER,
|
|
1461
|
+
});
|
|
1462
|
+
const body = stdout.trim();
|
|
1463
|
+
if (!body)
|
|
1464
|
+
return { ok: false, reason: "configured design command exited without stdout content" };
|
|
1465
|
+
return {
|
|
1466
|
+
ok: true,
|
|
1467
|
+
body,
|
|
1468
|
+
contentType: command.contentType,
|
|
1469
|
+
origin: `command:${command.safeLabel}`,
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
catch (error) {
|
|
1473
|
+
const code = isRecord(error) && (typeof error.code === "number" || typeof error.code === "string") ? String(error.code) : "unknown";
|
|
1474
|
+
return {
|
|
1475
|
+
ok: false,
|
|
1476
|
+
reason: `configured design command failed without exposing stdout/stderr content (code: ${code})`,
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
function originStringList(source, ...keys) {
|
|
1481
|
+
const origin = source.origin;
|
|
1482
|
+
const values = [];
|
|
1483
|
+
for (const key of keys) {
|
|
1484
|
+
values.push(...stringListValue(origin?.[key]));
|
|
1485
|
+
}
|
|
1486
|
+
return values;
|
|
1487
|
+
}
|
|
1488
|
+
function numberOriginValue(source, key) {
|
|
1489
|
+
const value = source.origin?.[key];
|
|
1490
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
|
1491
|
+
}
|
|
1492
|
+
function renderCommandTemplate(value, source, snapshot, figma) {
|
|
1493
|
+
const replacements = {
|
|
1494
|
+
url: firstOriginString(source, "url") ?? "",
|
|
1495
|
+
fileKey: figma?.fileKey ?? "",
|
|
1496
|
+
file_key: figma?.fileKey ?? "",
|
|
1497
|
+
nodeId: figma?.nodeId ?? "",
|
|
1498
|
+
node_id: figma?.nodeId ?? "",
|
|
1499
|
+
apiUrl: figma?.apiUrl ?? "",
|
|
1500
|
+
snapshot,
|
|
1501
|
+
};
|
|
1502
|
+
return value.replace(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g, (_, key) => replacements[key] ?? "");
|
|
1503
|
+
}
|
|
1504
|
+
function resolveCommandForTarget(target, command) {
|
|
1505
|
+
if (command.includes("/") || command.includes("\\"))
|
|
1506
|
+
return resolve(target, command);
|
|
1507
|
+
return command;
|
|
1508
|
+
}
|
|
1509
|
+
function trimTrailingSlash(value) {
|
|
1510
|
+
return value.replace(/\/+$/, "");
|
|
1511
|
+
}
|
|
1031
1512
|
function parseCsvRows(value) {
|
|
1032
1513
|
const rows = parseCsv(value).filter((row) => row.some((cell) => cell.trim().length > 0));
|
|
1033
1514
|
const headers = rows.shift() ?? [];
|
|
@@ -1118,10 +1599,11 @@ function firstCsvValue(row, ...keys) {
|
|
|
1118
1599
|
}
|
|
1119
1600
|
function firstOriginString(source, ...keys) {
|
|
1120
1601
|
const origin = source.origin;
|
|
1121
|
-
if (!origin)
|
|
1122
|
-
return undefined;
|
|
1123
1602
|
for (const key of keys) {
|
|
1124
|
-
const
|
|
1603
|
+
const sourceValue = source[key];
|
|
1604
|
+
if (typeof sourceValue === "string" && sourceValue.trim().length > 0)
|
|
1605
|
+
return sourceValue.trim();
|
|
1606
|
+
const value = origin?.[key];
|
|
1125
1607
|
if (typeof value === "string" && value.trim().length > 0)
|
|
1126
1608
|
return value.trim();
|
|
1127
1609
|
}
|
|
@@ -1142,9 +1624,13 @@ function authStringList(source, ...keys) {
|
|
|
1142
1624
|
for (const key of keys) {
|
|
1143
1625
|
values.push(...stringListValue(auth?.[key]));
|
|
1144
1626
|
}
|
|
1627
|
+
if (keys.some((key) => ["tokenEnv", "accessTokenEnv", "apiTokenEnv"].includes(key))) {
|
|
1628
|
+
values.push(...stringListValue(auth?.env));
|
|
1629
|
+
}
|
|
1145
1630
|
for (const key of keys) {
|
|
1146
1631
|
values.push(...stringListValue(source.origin?.[key]));
|
|
1147
1632
|
}
|
|
1633
|
+
values.push(...defaultAuthEnvNames(source, keys));
|
|
1148
1634
|
return Array.from(new Set(values));
|
|
1149
1635
|
}
|
|
1150
1636
|
function authCookieMap(source) {
|
|
@@ -1179,6 +1665,77 @@ function envValue(names) {
|
|
|
1179
1665
|
}
|
|
1180
1666
|
return {};
|
|
1181
1667
|
}
|
|
1668
|
+
function basicAuthHeaderFromEnvironment(source) {
|
|
1669
|
+
const username = envValue(authStringList(source, "usernameEnv", "userEnv", "basicUsernameEnv", "basicUserEnv"));
|
|
1670
|
+
const password = envValue(authStringList(source, "passwordEnv", "passEnv", "basicPasswordEnv", "basicPassEnv"));
|
|
1671
|
+
if (!username.value || !password.value)
|
|
1672
|
+
return { sources: [] };
|
|
1673
|
+
return {
|
|
1674
|
+
sources: [username.name, password.name].filter((name) => Boolean(name)),
|
|
1675
|
+
value: `Basic ${Buffer.from(`${username.value}:${password.value}`).toString("base64")}`,
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
function defaultAuthEnvNames(source, keys) {
|
|
1679
|
+
const provider = sourceProviderToken(source);
|
|
1680
|
+
if (!provider)
|
|
1681
|
+
return [];
|
|
1682
|
+
if (keys.some((key) => ["tokenEnv", "accessTokenEnv", "apiTokenEnv"].includes(key))) {
|
|
1683
|
+
if (provider.includes("figma"))
|
|
1684
|
+
return ["FIGMA_ACCESS_TOKEN", "FIGMA_TOKEN", "FIGMA_API_TOKEN"];
|
|
1685
|
+
if (provider.includes("gitlab"))
|
|
1686
|
+
return ["GITLAB_TOKEN", "GITLAB_ACCESS_TOKEN", "GITLAB_PRIVATE_TOKEN"];
|
|
1687
|
+
if (provider.includes("jira"))
|
|
1688
|
+
return ["ACCESS_TOKEN", "JIRA_ACCESS_TOKEN", "JIRA_TOKEN"];
|
|
1689
|
+
if (provider.includes("confluence"))
|
|
1690
|
+
return ["CONFLUENCE_ACCESS_TOKEN", "CONFLUENCE_TOKEN"];
|
|
1691
|
+
}
|
|
1692
|
+
if (keys.some((key) => ["usernameEnv", "userEnv", "basicUsernameEnv", "basicUserEnv"].includes(key))) {
|
|
1693
|
+
if (provider.includes("jira"))
|
|
1694
|
+
return ["JIRA_USER", "JIRA_USERNAME", "ATLASSIAN_EMAIL", "ATLASSIAN_USER"];
|
|
1695
|
+
if (provider.includes("confluence"))
|
|
1696
|
+
return ["CONFLUENCE_USER", "CONFLUENCE_USERNAME", "ATLASSIAN_EMAIL", "ATLASSIAN_USER"];
|
|
1697
|
+
}
|
|
1698
|
+
if (keys.some((key) => ["passwordEnv", "passEnv", "basicPasswordEnv", "basicPassEnv"].includes(key))) {
|
|
1699
|
+
if (provider.includes("jira"))
|
|
1700
|
+
return ["JIRA_API_TOKEN", "JIRA_PASSWORD", "ATLASSIAN_API_TOKEN", "ATLASSIAN_TOKEN"];
|
|
1701
|
+
if (provider.includes("confluence"))
|
|
1702
|
+
return ["CONFLUENCE_API_TOKEN", "CONFLUENCE_PASSWORD", "ATLASSIAN_API_TOKEN", "ATLASSIAN_TOKEN"];
|
|
1703
|
+
}
|
|
1704
|
+
return [];
|
|
1705
|
+
}
|
|
1706
|
+
function defaultTokenHeader(source) {
|
|
1707
|
+
const provider = sourceProviderToken(source);
|
|
1708
|
+
if (provider?.includes("figma"))
|
|
1709
|
+
return "X-Figma-Token";
|
|
1710
|
+
if (provider?.includes("gitlab"))
|
|
1711
|
+
return "PRIVATE-TOKEN";
|
|
1712
|
+
return "authorization";
|
|
1713
|
+
}
|
|
1714
|
+
function defaultTokenScheme(source, headerName) {
|
|
1715
|
+
const provider = sourceProviderToken(source);
|
|
1716
|
+
if (headerName.toLowerCase() !== "authorization")
|
|
1717
|
+
return "";
|
|
1718
|
+
if (provider?.includes("figma") || provider?.includes("gitlab"))
|
|
1719
|
+
return "";
|
|
1720
|
+
return "Bearer";
|
|
1721
|
+
}
|
|
1722
|
+
function sourceProviderToken(source) {
|
|
1723
|
+
const origin = inferSourceOrigin(source);
|
|
1724
|
+
return [
|
|
1725
|
+
origin.provider,
|
|
1726
|
+
origin.kind,
|
|
1727
|
+
source.origin?.provider,
|
|
1728
|
+
source.origin?.kind,
|
|
1729
|
+
source.kind,
|
|
1730
|
+
source.type,
|
|
1731
|
+
]
|
|
1732
|
+
.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
1733
|
+
.map((value) => value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""))
|
|
1734
|
+
.find((value) => value.includes("figma") ||
|
|
1735
|
+
value.includes("jira") ||
|
|
1736
|
+
value.includes("confluence") ||
|
|
1737
|
+
value.includes("gitlab"));
|
|
1738
|
+
}
|
|
1182
1739
|
function cookieHeaderFromEnvironment(source) {
|
|
1183
1740
|
const parts = [];
|
|
1184
1741
|
const sources = [];
|
|
@@ -1233,12 +1790,6 @@ function contentTypeForPath(path) {
|
|
|
1233
1790
|
return "text/html";
|
|
1234
1791
|
return "text/plain";
|
|
1235
1792
|
}
|
|
1236
|
-
function escapeTable(value) {
|
|
1237
|
-
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
1238
|
-
}
|
|
1239
|
-
function isRecord(value) {
|
|
1240
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1241
|
-
}
|
|
1242
1793
|
function renderRepositoryMetadata(entry, repository) {
|
|
1243
1794
|
return [
|
|
1244
1795
|
"---",
|
|
@@ -1272,6 +1823,7 @@ function htmlToStructuredMarkdown(value) {
|
|
|
1272
1823
|
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
1273
1824
|
.replace(/<(nav|header|footer|aside)[^>]*>[\s\S]*?<\/\1>/gi, "");
|
|
1274
1825
|
return body
|
|
1826
|
+
.replace(/<table\b[^>]*>[\s\S]*?<\/table>/gi, (table) => `\n${htmlTableToMarkdown(table)}\n`)
|
|
1275
1827
|
.replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "\n# $1\n")
|
|
1276
1828
|
.replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "\n## $1\n")
|
|
1277
1829
|
.replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, "\n### $1\n")
|
|
@@ -1282,14 +1834,60 @@ function htmlToStructuredMarkdown(value) {
|
|
|
1282
1834
|
.replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)")
|
|
1283
1835
|
.replace(/<[^>]+>/g, "")
|
|
1284
1836
|
.replace(/ /g, " ")
|
|
1837
|
+
.split("\n")
|
|
1838
|
+
.map((line) => decodeHtmlEntities(line).trim())
|
|
1839
|
+
.filter((line, index, lines) => line.length > 0 || lines[index - 1]?.length)
|
|
1840
|
+
.join("\n");
|
|
1841
|
+
}
|
|
1842
|
+
function htmlTableToMarkdown(table) {
|
|
1843
|
+
const rows = Array.from(table.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi))
|
|
1844
|
+
.map((row) => Array.from((row[1] ?? "").matchAll(/<(td|th)\b[^>]*>([\s\S]*?)<\/\1>/gi))
|
|
1845
|
+
.map((cell) => tableCellText(cell[2] ?? "")))
|
|
1846
|
+
.filter((row) => row.some((cell) => cell.length > 0));
|
|
1847
|
+
if (!rows.length)
|
|
1848
|
+
return "";
|
|
1849
|
+
const width = Math.max(...rows.map((row) => row.length));
|
|
1850
|
+
const normalized = rows.map((row) => {
|
|
1851
|
+
const copy = row.slice();
|
|
1852
|
+
while (copy.length < width)
|
|
1853
|
+
copy.push("");
|
|
1854
|
+
return copy;
|
|
1855
|
+
});
|
|
1856
|
+
const header = normalized[0] ?? [];
|
|
1857
|
+
if (!header.length)
|
|
1858
|
+
return "";
|
|
1859
|
+
const body = normalized.slice(1);
|
|
1860
|
+
return [
|
|
1861
|
+
`| ${header.join(" | ")} |`,
|
|
1862
|
+
`| ${header.map(() => "---").join(" | ")} |`,
|
|
1863
|
+
...body.map((row) => `| ${row.join(" | ")} |`),
|
|
1864
|
+
].join("\n");
|
|
1865
|
+
}
|
|
1866
|
+
function tableCellText(value) {
|
|
1867
|
+
return decodeHtmlEntities(stripHtml(value
|
|
1868
|
+
.replace(/<br\s*\/?>/gi, " ZSK_BR ")
|
|
1869
|
+
.replace(/<\/(p|div|section|li)>/gi, " ZSK_BR ")
|
|
1870
|
+
.replace(/<li\b[^>]*>/gi, "- ")))
|
|
1871
|
+
.replace(/\s*ZSK_BR\s*/g, "; ")
|
|
1872
|
+
.replace(/[ \t\r\n]+/g, " ")
|
|
1873
|
+
.replace(/(?:\s*;\s*){2,}/g, "; ")
|
|
1874
|
+
.replace(/^\s*;\s*|\s*;\s*$/g, "")
|
|
1875
|
+
.replace(/\|/g, "\\|")
|
|
1876
|
+
.trim();
|
|
1877
|
+
}
|
|
1878
|
+
function stripHtml(value) {
|
|
1879
|
+
return value.replace(/<[^>]+>/g, "");
|
|
1880
|
+
}
|
|
1881
|
+
function decodeHtmlEntities(value) {
|
|
1882
|
+
return value
|
|
1883
|
+
.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)))
|
|
1884
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_, code) => String.fromCodePoint(parseInt(code, 16)))
|
|
1885
|
+
.replace(/ /g, " ")
|
|
1285
1886
|
.replace(/&/g, "&")
|
|
1286
1887
|
.replace(/</g, "<")
|
|
1287
1888
|
.replace(/>/g, ">")
|
|
1288
1889
|
.replace(/"/g, "\"")
|
|
1289
|
-
.
|
|
1290
|
-
.map((line) => line.trim())
|
|
1291
|
-
.filter((line, index, lines) => line.length > 0 || lines[index - 1]?.length)
|
|
1292
|
-
.join("\n");
|
|
1890
|
+
.replace(/'/g, "'");
|
|
1293
1891
|
}
|
|
1294
1892
|
function fencedBody(body, contentType) {
|
|
1295
1893
|
const info = contentType.includes("json")
|
|
@@ -1307,25 +1905,6 @@ function shouldPreserveMachineReadable(source, originPath, snapshotPath) {
|
|
|
1307
1905
|
[".json", ".yaml", ".yml"].includes(ext) ||
|
|
1308
1906
|
[".json", ".yaml", ".yml"].includes(snapshotExt));
|
|
1309
1907
|
}
|
|
1310
|
-
function renderDownstreamImpact(results) {
|
|
1311
|
-
const changed = results.filter((result) => result.changed);
|
|
1312
|
-
const blocked = results.filter((result) => ["blocked-auth", "source-gap", "failed"].includes(result.status));
|
|
1313
|
-
return [
|
|
1314
|
-
"# Downstream Impact",
|
|
1315
|
-
"",
|
|
1316
|
-
`Changed sources: ${changed.length}`,
|
|
1317
|
-
`Blocked sources: ${blocked.length}`,
|
|
1318
|
-
"",
|
|
1319
|
-
changed.length === 0
|
|
1320
|
-
? "No downstream refresh is recommended from this sync run."
|
|
1321
|
-
: changed.map((result) => `- \`${result.sourcePath}\`: snapshot changed; review dependent proposal/spec/design/tasks/tests before claiming freshness.`).join("\n"),
|
|
1322
|
-
"",
|
|
1323
|
-
blocked.length === 0
|
|
1324
|
-
? "No blocked sources."
|
|
1325
|
-
: blocked.map((result) => `- \`${result.sourcePath}\` [${result.status}]: ${result.reason}`).join("\n"),
|
|
1326
|
-
"",
|
|
1327
|
-
].join("\n");
|
|
1328
|
-
}
|
|
1329
1908
|
async function cookieHeaderFromStorageState(storageStatePath, url) {
|
|
1330
1909
|
try {
|
|
1331
1910
|
const parsed = JSON.parse(await readFile(storageStatePath, "utf8"));
|
|
@@ -1409,74 +1988,46 @@ async function loadProjectPlaywright(target) {
|
|
|
1409
1988
|
}
|
|
1410
1989
|
return await import(resolved);
|
|
1411
1990
|
}
|
|
1412
|
-
async function
|
|
1991
|
+
async function safeStat(path) {
|
|
1413
1992
|
try {
|
|
1414
|
-
|
|
1993
|
+
const info = await stat(path);
|
|
1994
|
+
return { isDirectory: info.isDirectory(), isFile: info.isFile() };
|
|
1415
1995
|
}
|
|
1416
1996
|
catch {
|
|
1417
|
-
return
|
|
1997
|
+
return null;
|
|
1418
1998
|
}
|
|
1419
1999
|
}
|
|
1420
|
-
|
|
1421
|
-
const
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
}
|
|
1429
|
-
catch {
|
|
1430
|
-
return false;
|
|
1431
|
-
}
|
|
2000
|
+
function isGitSubmoduleResource(source) {
|
|
2001
|
+
const values = [
|
|
2002
|
+
source.kind,
|
|
2003
|
+
source.type,
|
|
2004
|
+
source.origin?.kind,
|
|
2005
|
+
source.metadata?.resourceType,
|
|
2006
|
+
].map((value) => typeof value === "string" ? value.toLowerCase() : "");
|
|
2007
|
+
return values.some((value) => value === "git-submodule" || value === "submodule");
|
|
1432
2008
|
}
|
|
1433
|
-
async function
|
|
2009
|
+
async function updateGitSubmodule(target, sourcePath) {
|
|
1434
2010
|
try {
|
|
1435
|
-
|
|
2011
|
+
await execFileAsync("git", ["-C", target, "submodule", "update", "--init", "--remote", "--", sourcePath], {
|
|
2012
|
+
timeout: 120_000,
|
|
2013
|
+
maxBuffer: DEFAULT_ADAPTER_COMMAND_MAX_BUFFER,
|
|
2014
|
+
});
|
|
2015
|
+
return { ok: true };
|
|
1436
2016
|
}
|
|
1437
|
-
catch {
|
|
1438
|
-
|
|
2017
|
+
catch (error) {
|
|
2018
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2019
|
+
return { ok: false, reason: `git submodule update failed for ${sourcePath}: ${reason}` };
|
|
1439
2020
|
}
|
|
1440
2021
|
}
|
|
1441
|
-
function safeFileName(value) {
|
|
1442
|
-
return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "source";
|
|
1443
|
-
}
|
|
1444
|
-
function isMigrationLaneCandidate(value, staleNames) {
|
|
1445
|
-
return (staleNames.has(value) ||
|
|
1446
|
-
/^v?\d+(?:[._-]\d+)*$/i.test(value) ||
|
|
1447
|
-
/^(sprint|iteration|release)[-_]?\d*/i.test(value));
|
|
1448
|
-
}
|
|
1449
|
-
function resolveAuthStatePath(target, config, authState) {
|
|
1450
|
-
const statePath = resolve(target, authState);
|
|
1451
|
-
const sharedAuthRoot = resolve(target, getWorkspacePath(config, "playwrightRoot"), ".auth");
|
|
1452
|
-
const modulesRoot = resolve(target, getWorkspacePath(config, "modulesRoot"));
|
|
1453
|
-
const pathParts = statePath.split(/[\\/]/);
|
|
1454
|
-
const modulePrivate = isInside(modulesRoot, statePath) && pathParts.includes("_playwright") && pathParts.includes(".auth");
|
|
1455
|
-
if (isInside(sharedAuthRoot, statePath) || modulePrivate)
|
|
1456
|
-
return statePath;
|
|
1457
|
-
throw new Error(`Playwright storageState input must stay under ${sharedAuthRoot} or a module _playwright/.auth directory`);
|
|
1458
|
-
}
|
|
1459
2022
|
function resolveSelectedAuthStatePath(target, config, opts) {
|
|
1460
2023
|
if (opts.authState)
|
|
1461
|
-
return
|
|
2024
|
+
return resolvePlaywrightAuthStatePath(target, config, opts.authState);
|
|
1462
2025
|
if (opts.authProfile)
|
|
1463
|
-
return
|
|
2026
|
+
return resolvePlaywrightAuthProfilePath(target, config, opts.authProfile);
|
|
1464
2027
|
return undefined;
|
|
1465
2028
|
}
|
|
1466
|
-
function resolveAuthProfilePath(target, config, profile) {
|
|
1467
|
-
const authRoot = resolve(target, getWorkspacePath(config, "playwrightRoot"), ".auth");
|
|
1468
|
-
const fileName = profile ? `${safeFileName(profile)}.json` : "user_data.json";
|
|
1469
|
-
return join(authRoot, fileName);
|
|
1470
|
-
}
|
|
1471
2029
|
function resolveAdapterLocalFilePath(target, value) {
|
|
1472
2030
|
const path = resolve(target, value);
|
|
1473
|
-
return
|
|
1474
|
-
}
|
|
1475
|
-
function isInside(root, target) {
|
|
1476
|
-
const rel = relative(root, target);
|
|
1477
|
-
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
1478
|
-
}
|
|
1479
|
-
export function createSyncRunId(now = new Date()) {
|
|
1480
|
-
return now.toISOString().replace(/[:.]/g, "-");
|
|
2031
|
+
return isPathInside(resolve(target), path) ? path : undefined;
|
|
1481
2032
|
}
|
|
1482
2033
|
//# sourceMappingURL=prepare-sync.js.map
|