ai-project-maintainer 0.4.0 → 0.4.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/README.md +16 -6
- package/ai-project-maintainer/agents/openai.yaml +6 -6
- package/ai-project-maintainer/references/ci-guardrails.md +55 -55
- package/ai-project-maintainer/references/database.md +60 -60
- package/ai-project-maintainer/references/electron-desktop.md +43 -43
- package/ai-project-maintainer/references/incident-response.md +52 -52
- package/ai-project-maintainer/references/security.md +48 -48
- package/ai-project-maintainer/references/tool-router.md +53 -53
- package/ai-project-maintainer/scripts/bootstrap-local-tools.ps1 +109 -109
- package/ai-project-maintainer/scripts/ci-smoke-gate.mjs +26 -26
- package/ai-project-maintainer/scripts/init-project.mjs +30 -18
- package/ai-project-maintainer/scripts/lib/check-registry.mjs +10 -9
- package/ai-project-maintainer/scripts/lib/checks.mjs +22 -10
- package/ai-project-maintainer/scripts/lib/command-runner.mjs +17 -3
- package/ai-project-maintainer/scripts/lib/policy.mjs +6 -4
- package/ai-project-maintainer/scripts/lib/report.mjs +56 -32
- package/assets/demo-90s-storyboard.svg +98 -0
- package/assets/demo-90s.gif +0 -0
- package/assets/social-preview.png +0 -0
- package/assets/social-preview.svg +55 -0
- package/docs/DEMO.md +68 -61
- package/docs/DEMO.zh-CN.md +75 -69
- package/docs/GITHUB-LAUNCH-CHECKLIST.md +11 -11
- package/docs/POLICY-AND-EXCEPTIONS.zh-CN.md +1 -1
- package/docs/PROMOTION.md +49 -21
- package/docs/SECURITY-WORKFLOW.md +61 -59
- package/docs/UPGRADE-ROADMAP.zh-CN.md +58 -58
- package/docs/demo-output/90-second-demo.html +187 -0
- package/docs/demo-output/before-after-case.md +91 -0
- package/docs/demo-output/security-report.md +62 -61
- package/docs/superpowers/plans/2026-06-29-ci-dogfooding.md +200 -200
- package/examples/demo-ai-app/.ai-maintainer/business-flows.yml +14 -14
- package/examples/demo-ai-app/.ai-maintainer/db-migration-policy.yml +6 -6
- package/examples/demo-ai-app/.ai-maintainer/evidence-sources.yml +18 -18
- package/examples/demo-ai-app/.ai-maintainer/exceptions.yml +1 -1
- package/examples/demo-ai-app/.ai-maintainer/incident-runbook.md +11 -11
- package/examples/demo-ai-app/.ai-maintainer/observability-checklist.yml +7 -7
- package/examples/demo-ai-app/.ai-maintainer/policy.yml +27 -27
- package/examples/demo-ai-app/.ai-maintainer/project-profile.yml +15 -15
- package/examples/demo-ai-app/.ai-maintainer/release-checklist.yml +7 -7
- package/examples/demo-ai-app/.ai-maintainer/risk-policy.yml +5 -5
- package/examples/demo-ai-app/.ai-maintainer/threat-model.md +18 -18
- package/examples/demo-ai-app/README.md +38 -38
- package/examples/demo-ai-app/package-lock.json +15 -15
- package/examples/demo-ai-app/package.json +16 -16
- package/examples/demo-ai-app/scripts/build.mjs +18 -18
- package/examples/demo-ai-app/scripts/create-before-state.mjs +86 -86
- package/examples/demo-ai-app/scripts/run-demo-gate.mjs +95 -95
- package/examples/demo-ai-app/src/order-risk.js +28 -28
- package/examples/demo-ai-app/test/order-risk.test.mjs +24 -24
- package/package.json +2 -1
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
profile: oss
|
|
2
|
-
mode: strict
|
|
3
|
-
checks:
|
|
4
|
-
gitleaks: block
|
|
5
|
-
trivy: block
|
|
6
|
-
semgrep: block
|
|
7
|
-
osv-scanner: warn
|
|
8
|
-
syft: warn
|
|
9
|
-
grype: warn
|
|
10
|
-
actionlint: block
|
|
11
|
-
zizmor: warn
|
|
12
|
-
checkov: warn
|
|
13
|
-
trivy-config: warn
|
|
14
|
-
scorecard: warn
|
|
15
|
-
megalinter: warn
|
|
16
|
-
pre-commit: warn
|
|
17
|
-
package-audit: warn
|
|
18
|
-
fail_on:
|
|
19
|
-
tests: true
|
|
20
|
-
secrets: true
|
|
21
|
-
dependency_high_or_critical: true
|
|
22
|
-
semgrep_blocking: true
|
|
23
|
-
trivy_unavailable: true
|
|
24
|
-
electron_dangerous_settings: true
|
|
25
|
-
ci_security_high: true
|
|
26
|
-
warn_on:
|
|
27
|
-
missing_optional_tools: true
|
|
1
|
+
profile: oss
|
|
2
|
+
mode: strict
|
|
3
|
+
checks:
|
|
4
|
+
gitleaks: block
|
|
5
|
+
trivy: block
|
|
6
|
+
semgrep: block
|
|
7
|
+
osv-scanner: warn
|
|
8
|
+
syft: warn
|
|
9
|
+
grype: warn
|
|
10
|
+
actionlint: block
|
|
11
|
+
zizmor: warn
|
|
12
|
+
checkov: warn
|
|
13
|
+
trivy-config: warn
|
|
14
|
+
scorecard: warn
|
|
15
|
+
megalinter: warn
|
|
16
|
+
pre-commit: warn
|
|
17
|
+
package-audit: warn
|
|
18
|
+
fail_on:
|
|
19
|
+
tests: true
|
|
20
|
+
secrets: true
|
|
21
|
+
dependency_high_or_critical: true
|
|
22
|
+
semgrep_blocking: true
|
|
23
|
+
trivy_unavailable: true
|
|
24
|
+
electron_dangerous_settings: true
|
|
25
|
+
ci_security_high: true
|
|
26
|
+
warn_on:
|
|
27
|
+
missing_optional_tools: true
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
schema_version: 1
|
|
2
|
-
project:
|
|
3
|
-
name: demo-ai-app
|
|
4
|
-
type: node
|
|
5
|
-
lifecycle: production-candidate
|
|
6
|
-
production: true
|
|
7
|
-
risk:
|
|
8
|
-
handles_auth: false
|
|
9
|
-
handles_sensitive_data: false
|
|
10
|
-
handles_payments: false
|
|
11
|
-
handles_financial_data: false
|
|
12
|
-
handles_health_data: false
|
|
13
|
-
has_database: false
|
|
14
|
-
has_deployment: true
|
|
15
|
-
has_user_generated_content: false
|
|
1
|
+
schema_version: 1
|
|
2
|
+
project:
|
|
3
|
+
name: demo-ai-app
|
|
4
|
+
type: node
|
|
5
|
+
lifecycle: production-candidate
|
|
6
|
+
production: true
|
|
7
|
+
risk:
|
|
8
|
+
handles_auth: false
|
|
9
|
+
handles_sensitive_data: false
|
|
10
|
+
handles_payments: false
|
|
11
|
+
handles_financial_data: false
|
|
12
|
+
handles_health_data: false
|
|
13
|
+
has_database: false
|
|
14
|
+
has_deployment: true
|
|
15
|
+
has_user_generated_content: false
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
schema_version: 1
|
|
2
|
-
release:
|
|
3
|
-
has_staging: true
|
|
4
|
-
has_smoke_tests: true
|
|
5
|
-
has_manual_approval: false
|
|
6
|
-
has_rollback_plan: false
|
|
7
|
-
has_versioned_artifacts: true
|
|
1
|
+
schema_version: 1
|
|
2
|
+
release:
|
|
3
|
+
has_staging: true
|
|
4
|
+
has_smoke_tests: true
|
|
5
|
+
has_manual_approval: false
|
|
6
|
+
has_rollback_plan: false
|
|
7
|
+
has_versioned_artifacts: true
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
schema_version: 1
|
|
2
|
-
production:
|
|
3
|
-
block_on_coverage_gaps: false
|
|
4
|
-
block_on_user_decisions: false
|
|
5
|
-
require_intake: true
|
|
1
|
+
schema_version: 1
|
|
2
|
+
production:
|
|
3
|
+
block_on_coverage_gaps: false
|
|
4
|
+
block_on_user_decisions: false
|
|
5
|
+
require_intake: true
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
# Threat Model
|
|
2
|
-
|
|
3
|
-
## Assets
|
|
4
|
-
|
|
5
|
-
- Order totals
|
|
6
|
-
- Payment status
|
|
7
|
-
- Release decisions
|
|
8
|
-
|
|
9
|
-
## Trust Boundaries
|
|
10
|
-
|
|
11
|
-
- Browser or API client input
|
|
12
|
-
- Order risk calculation
|
|
13
|
-
- Release approval process
|
|
14
|
-
|
|
15
|
-
## User Decisions
|
|
16
|
-
|
|
17
|
-
- Confirm whether manual review threshold matches real business risk.
|
|
18
|
-
- Confirm who can override a release hold.
|
|
1
|
+
# Threat Model
|
|
2
|
+
|
|
3
|
+
## Assets
|
|
4
|
+
|
|
5
|
+
- Order totals
|
|
6
|
+
- Payment status
|
|
7
|
+
- Release decisions
|
|
8
|
+
|
|
9
|
+
## Trust Boundaries
|
|
10
|
+
|
|
11
|
+
- Browser or API client input
|
|
12
|
+
- Order risk calculation
|
|
13
|
+
- Release approval process
|
|
14
|
+
|
|
15
|
+
## User Decisions
|
|
16
|
+
|
|
17
|
+
- Confirm whether manual review threshold matches real business risk.
|
|
18
|
+
- Confirm who can override a release hold.
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
# Demo AI App
|
|
2
|
-
|
|
3
|
-
This is a small healthy Node.js project used by AI Project Maintainer demos.
|
|
4
|
-
|
|
5
|
-
It has:
|
|
6
|
-
|
|
7
|
-
- business-critical tests
|
|
8
|
-
- a build script
|
|
9
|
-
- production audit intake files
|
|
10
|
-
- intentional production evidence gaps for monitoring, alerts, approval, and rollback
|
|
11
|
-
|
|
12
|
-
## Run The Healthy Project
|
|
13
|
-
|
|
14
|
-
```powershell
|
|
15
|
-
npm test --prefix .\examples\demo-ai-app
|
|
16
|
-
npm run build --prefix .\examples\demo-ai-app
|
|
17
|
-
node .\examples\demo-ai-app\scripts\run-demo-gate.mjs
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
The demo gate uses temporary scanner shims so the sample report is reproducible on machines that do not have Gitleaks, Trivy, Semgrep, and other scanners installed yet.
|
|
21
|
-
|
|
22
|
-
## Generate The Broken Before State
|
|
23
|
-
|
|
24
|
-
```powershell
|
|
25
|
-
node .\examples\demo-ai-app\scripts\create-before-state.mjs
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
The command prints a temporary directory. Run `npm test` in that copied project to see the business tests fail. Nothing bad is committed into this repository; the failing state exists only under the OS temp directory.
|
|
29
|
-
|
|
30
|
-
## Run The Real Gate
|
|
31
|
-
|
|
32
|
-
When scanner CLIs are installed, run the same command a real project would use:
|
|
33
|
-
|
|
34
|
-
```powershell
|
|
35
|
-
npx ai-project-maintainer gate .\examples\demo-ai-app --production --strict --release --output reports/security-report.json
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
The expected result is PASS with visible GAP items for missing production evidence.
|
|
1
|
+
# Demo AI App
|
|
2
|
+
|
|
3
|
+
This is a small healthy Node.js project used by AI Project Maintainer demos.
|
|
4
|
+
|
|
5
|
+
It has:
|
|
6
|
+
|
|
7
|
+
- business-critical tests
|
|
8
|
+
- a build script
|
|
9
|
+
- production audit intake files
|
|
10
|
+
- intentional production evidence gaps for monitoring, alerts, approval, and rollback
|
|
11
|
+
|
|
12
|
+
## Run The Healthy Project
|
|
13
|
+
|
|
14
|
+
```powershell
|
|
15
|
+
npm test --prefix .\examples\demo-ai-app
|
|
16
|
+
npm run build --prefix .\examples\demo-ai-app
|
|
17
|
+
node .\examples\demo-ai-app\scripts\run-demo-gate.mjs
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The demo gate uses temporary scanner shims so the sample report is reproducible on machines that do not have Gitleaks, Trivy, Semgrep, and other scanners installed yet.
|
|
21
|
+
|
|
22
|
+
## Generate The Broken Before State
|
|
23
|
+
|
|
24
|
+
```powershell
|
|
25
|
+
node .\examples\demo-ai-app\scripts\create-before-state.mjs
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The command prints a temporary directory. Run `npm test` in that copied project to see the business tests fail. Nothing bad is committed into this repository; the failing state exists only under the OS temp directory.
|
|
29
|
+
|
|
30
|
+
## Run The Real Gate
|
|
31
|
+
|
|
32
|
+
When scanner CLIs are installed, run the same command a real project would use:
|
|
33
|
+
|
|
34
|
+
```powershell
|
|
35
|
+
npx ai-project-maintainer gate .\examples\demo-ai-app --production --strict --release --output reports/security-report.json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The expected result is PASS with visible GAP items for missing production evidence.
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "demo-ai-app",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"lockfileVersion": 3,
|
|
5
|
-
"requires": true,
|
|
6
|
-
"packages": {
|
|
7
|
-
"": {
|
|
8
|
-
"name": "demo-ai-app",
|
|
9
|
-
"version": "0.1.0",
|
|
10
|
-
"engines": {
|
|
11
|
-
"node": ">=20"
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "demo-ai-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "demo-ai-app",
|
|
9
|
+
"version": "0.1.0",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=20"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "demo-ai-app",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"description": "Runnable demo project for AI Project Maintainer.",
|
|
7
|
-
"scripts": {
|
|
8
|
-
"test": "node --test",
|
|
9
|
-
"build": "node scripts/build.mjs",
|
|
10
|
-
"demo:before": "node scripts/create-before-state.mjs",
|
|
11
|
-
"demo:gate": "node scripts/run-demo-gate.mjs"
|
|
12
|
-
},
|
|
13
|
-
"engines": {
|
|
14
|
-
"node": ">=20"
|
|
15
|
-
}
|
|
16
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "demo-ai-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"description": "Runnable demo project for AI Project Maintainer.",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "node --test",
|
|
9
|
+
"build": "node scripts/build.mjs",
|
|
10
|
+
"demo:before": "node scripts/create-before-state.mjs",
|
|
11
|
+
"demo:gate": "node scripts/run-demo-gate.mjs"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
-
const outDir = path.join(root, "dist");
|
|
7
|
-
|
|
8
|
-
fs.mkdirSync(outDir, { recursive: true });
|
|
9
|
-
fs.writeFileSync(
|
|
10
|
-
path.join(outDir, "build-manifest.json"),
|
|
11
|
-
`${JSON.stringify({
|
|
12
|
-
app: "demo-ai-app",
|
|
13
|
-
builtAt: new Date().toISOString(),
|
|
14
|
-
entrypoints: ["src/order-risk.js"],
|
|
15
|
-
}, null, 2)}\n`,
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
console.log(`Demo build manifest written to ${path.relative(root, outDir)}`);
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
+
const outDir = path.join(root, "dist");
|
|
7
|
+
|
|
8
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
9
|
+
fs.writeFileSync(
|
|
10
|
+
path.join(outDir, "build-manifest.json"),
|
|
11
|
+
`${JSON.stringify({
|
|
12
|
+
app: "demo-ai-app",
|
|
13
|
+
builtAt: new Date().toISOString(),
|
|
14
|
+
entrypoints: ["src/order-risk.js"],
|
|
15
|
+
}, null, 2)}\n`,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
console.log(`Demo build manifest written to ${path.relative(root, outDir)}`);
|
|
@@ -1,86 +1,86 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from "node:fs";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
|
|
7
|
-
const ignored = new Set(["node_modules", "dist", "reports"]);
|
|
8
|
-
|
|
9
|
-
function normalizeForCompare(value) {
|
|
10
|
-
return path.resolve(value).toLowerCase();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function assertInsideTemp(destination) {
|
|
14
|
-
const tempRoot = normalizeForCompare(fs.realpathSync(os.tmpdir()));
|
|
15
|
-
const resolved = normalizeForCompare(destination);
|
|
16
|
-
if (resolved !== tempRoot && !resolved.startsWith(`${tempRoot}${path.sep}`)) {
|
|
17
|
-
throw new Error(`Refusing to write demo before-state outside the OS temp directory: ${destination}`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function copyDirectory(source, destination) {
|
|
22
|
-
fs.mkdirSync(destination, { recursive: true });
|
|
23
|
-
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
24
|
-
if (ignored.has(entry.name)) continue;
|
|
25
|
-
const from = path.join(source, entry.name);
|
|
26
|
-
const to = path.join(destination, entry.name);
|
|
27
|
-
if (entry.isDirectory()) copyDirectory(from, to);
|
|
28
|
-
else if (entry.isFile()) fs.copyFileSync(from, to);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function readOutputArg(args) {
|
|
33
|
-
const index = args.indexOf("--output");
|
|
34
|
-
if (index !== -1) return args[index + 1];
|
|
35
|
-
const inline = args.find((arg) => arg.startsWith("--output="));
|
|
36
|
-
return inline ? inline.slice("--output=".length) : null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function createBeforeState({ outputPath = null } = {}) {
|
|
40
|
-
const demoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
41
|
-
const destination = path.resolve(outputPath || path.join(os.tmpdir(), `apm-demo-before-${Date.now()}`));
|
|
42
|
-
assertInsideTemp(destination);
|
|
43
|
-
|
|
44
|
-
fs.rmSync(destination, { recursive: true, force: true });
|
|
45
|
-
copyDirectory(demoRoot, destination);
|
|
46
|
-
|
|
47
|
-
fs.writeFileSync(
|
|
48
|
-
path.join(destination, "src", "order-risk.js"),
|
|
49
|
-
`const shippingRates = {
|
|
50
|
-
standard: 499,
|
|
51
|
-
expedited: 1299,
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export function quoteOrder({ subtotalCents, shippingTier }) {
|
|
55
|
-
if (!Number.isInteger(subtotalCents) || subtotalCents < 0) {
|
|
56
|
-
throw new TypeError("subtotalCents must be a non-negative integer");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (!Object.hasOwn(shippingRates, shippingTier)) {
|
|
60
|
-
throw new RangeError(\`unsupported shipping tier: \${shippingTier}\`);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const shippingCents = shippingRates[shippingTier];
|
|
64
|
-
const totalCents = subtotalCents + shippingCents;
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
subtotalCents,
|
|
68
|
-
shippingCents,
|
|
69
|
-
totalCents,
|
|
70
|
-
needsManualReview: false,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function canReleaseOrder({ paid, flagged }) {
|
|
75
|
-
return Boolean(paid && !flagged);
|
|
76
|
-
}
|
|
77
|
-
`,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
return { destination };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
84
|
-
const result = createBeforeState({ outputPath: readOutputArg(process.argv.slice(2)) });
|
|
85
|
-
console.log(JSON.stringify(result, null, 2));
|
|
86
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const ignored = new Set(["node_modules", "dist", "reports"]);
|
|
8
|
+
|
|
9
|
+
function normalizeForCompare(value) {
|
|
10
|
+
return path.resolve(value).toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function assertInsideTemp(destination) {
|
|
14
|
+
const tempRoot = normalizeForCompare(fs.realpathSync(os.tmpdir()));
|
|
15
|
+
const resolved = normalizeForCompare(destination);
|
|
16
|
+
if (resolved !== tempRoot && !resolved.startsWith(`${tempRoot}${path.sep}`)) {
|
|
17
|
+
throw new Error(`Refusing to write demo before-state outside the OS temp directory: ${destination}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function copyDirectory(source, destination) {
|
|
22
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
23
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
24
|
+
if (ignored.has(entry.name)) continue;
|
|
25
|
+
const from = path.join(source, entry.name);
|
|
26
|
+
const to = path.join(destination, entry.name);
|
|
27
|
+
if (entry.isDirectory()) copyDirectory(from, to);
|
|
28
|
+
else if (entry.isFile()) fs.copyFileSync(from, to);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readOutputArg(args) {
|
|
33
|
+
const index = args.indexOf("--output");
|
|
34
|
+
if (index !== -1) return args[index + 1];
|
|
35
|
+
const inline = args.find((arg) => arg.startsWith("--output="));
|
|
36
|
+
return inline ? inline.slice("--output=".length) : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createBeforeState({ outputPath = null } = {}) {
|
|
40
|
+
const demoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
41
|
+
const destination = path.resolve(outputPath || path.join(os.tmpdir(), `apm-demo-before-${Date.now()}`));
|
|
42
|
+
assertInsideTemp(destination);
|
|
43
|
+
|
|
44
|
+
fs.rmSync(destination, { recursive: true, force: true });
|
|
45
|
+
copyDirectory(demoRoot, destination);
|
|
46
|
+
|
|
47
|
+
fs.writeFileSync(
|
|
48
|
+
path.join(destination, "src", "order-risk.js"),
|
|
49
|
+
`const shippingRates = {
|
|
50
|
+
standard: 499,
|
|
51
|
+
expedited: 1299,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function quoteOrder({ subtotalCents, shippingTier }) {
|
|
55
|
+
if (!Number.isInteger(subtotalCents) || subtotalCents < 0) {
|
|
56
|
+
throw new TypeError("subtotalCents must be a non-negative integer");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (!Object.hasOwn(shippingRates, shippingTier)) {
|
|
60
|
+
throw new RangeError(\`unsupported shipping tier: \${shippingTier}\`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const shippingCents = shippingRates[shippingTier];
|
|
64
|
+
const totalCents = subtotalCents + shippingCents;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
subtotalCents,
|
|
68
|
+
shippingCents,
|
|
69
|
+
totalCents,
|
|
70
|
+
needsManualReview: false,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function canReleaseOrder({ paid, flagged }) {
|
|
75
|
+
return Boolean(paid && !flagged);
|
|
76
|
+
}
|
|
77
|
+
`,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return { destination };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
84
|
+
const result = createBeforeState({ outputPath: readOutputArg(process.argv.slice(2)) });
|
|
85
|
+
console.log(JSON.stringify(result, null, 2));
|
|
86
|
+
}
|