@warpgogol/forge 2.8.0 → 2.8.2
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/AGENTS.md +1 -1
- package/README.md +23 -13
- package/README.uk.md +19 -12
- package/dist/os/session/handlers/validate.d.ts.map +1 -1
- package/dist/os/session/handlers/validate.js +23 -1
- package/dist/os/session/handlers/validate.js.map +1 -1
- package/dist/os/session/types.d.ts +31 -1
- package/dist/os/session/types.d.ts.map +1 -1
- package/dist/os/session/types.js +7 -0
- package/dist/os/session/types.js.map +1 -1
- package/os/adr/adr.module.ts +158 -0
- package/os/adr/frontmatter-io.ts +80 -0
- package/os/adr/handlers/archive.ts +244 -0
- package/os/adr/handlers/implement-stamp.ts +331 -0
- package/os/adr/handlers/list-create.ts +207 -0
- package/os/adr/handlers/validate.test.ts +232 -0
- package/os/adr/handlers/validate.ts +429 -0
- package/os/adr/index.ts +33 -0
- package/os/adr/types.ts +141 -0
- package/os/audit/audit.module.ts +48 -0
- package/os/audit/frontmatter-io.ts +84 -0
- package/os/audit/handlers/archive.ts +239 -0
- package/os/audit/index.ts +23 -0
- package/os/audit/types.ts +29 -0
- package/os/compass/compass.module.ts +186 -0
- package/os/compass/handlers/compass-audit-handler.ts +382 -0
- package/os/compass/handlers/compass-change-summary-handler.ts +272 -0
- package/os/compass/handlers/compass-inventory-handler.ts +294 -0
- package/os/compass/handlers/compass-inventory.ts +521 -0
- package/os/compass/handlers/git-revision.ts +137 -0
- package/os/compass/handlers/resolve-scan-root-workpiece.test.ts +65 -0
- package/os/compass/handlers/resolve-scan-root.ts +79 -0
- package/os/compass/index.ts +33 -0
- package/os/core/core.module.ts +948 -0
- package/os/core/handlers/assets-check.ts +122 -0
- package/os/core/handlers/assets-helpers.ts +174 -0
- package/os/core/handlers/assets-list.ts +99 -0
- package/os/core/handlers/build.ts +155 -0
- package/os/core/handlers/determinism-check.ts +354 -0
- package/os/core/handlers/dev.ts +127 -0
- package/os/core/handlers/invariant-engine.test.ts +681 -0
- package/os/core/handlers/knowledge-compact.ts +248 -0
- package/os/core/handlers/lifecycle-handlers.test.ts +524 -0
- package/os/core/handlers/note-frontmatter-validate.test.ts +93 -0
- package/os/core/handlers/note-link-validate.test.ts +117 -0
- package/os/core/handlers/note-orphan-detect.test.ts +94 -0
- package/os/core/handlers/package-health.test.ts +226 -0
- package/os/core/handlers/package-health.ts +231 -0
- package/os/core/handlers/pinned-check.ts +200 -0
- package/os/core/handlers/pinned-init.ts +333 -0
- package/os/core/handlers/pinned-types.ts +50 -0
- package/os/core/handlers/pinned-validate.ts +301 -0
- package/os/core/handlers/profile-resolve.ts +92 -0
- package/os/core/handlers/release-prepare.ts +294 -0
- package/os/core/handlers/release-publish.ts +211 -0
- package/os/core/handlers/validate.ts +262 -0
- package/os/core/handlers/workspace-deps.ts +55 -0
- package/os/core/index.ts +13 -0
- package/os/exploration/exploration.module.ts +90 -0
- package/os/exploration/frontmatter-io.ts +77 -0
- package/os/exploration/handlers/archive.ts +159 -0
- package/os/exploration/handlers/list.ts +77 -0
- package/os/exploration/handlers/show.ts +107 -0
- package/os/exploration/index.ts +25 -0
- package/os/exploration/types.ts +65 -0
- package/os/mission/handlers/archive.test.ts +391 -0
- package/os/mission/handlers/archive.ts +453 -0
- package/os/mission/index.ts +1 -0
- package/os/mission/mission.module.ts +48 -0
- package/os/mission/types.ts +41 -0
- package/os/naming/index.ts +14 -0
- package/os/naming/naming-convention.test.ts +163 -0
- package/os/naming/naming-convention.ts +378 -0
- package/os/naming/naming.module.ts +36 -0
- package/os/notes/index.ts +10 -0
- package/os/notes/notes.module.ts +109 -0
- package/os/plan/frontmatter-io.ts +82 -0
- package/os/plan/handlers/archive.ts +238 -0
- package/os/plan/index.ts +23 -0
- package/os/plan/plan.module.ts +48 -0
- package/os/plan/types.ts +29 -0
- package/os/program/discovery.ts +294 -0
- package/os/program/handlers/complete.ts +419 -0
- package/os/program/handlers/lease.ts +433 -0
- package/os/program/handlers/seal.ts +332 -0
- package/os/program/handlers/validate.ts +213 -0
- package/os/program/lease.ts +156 -0
- package/os/program/program.module.ts +212 -0
- package/os/program/schemas.ts +218 -0
- package/os/program/state.ts +264 -0
- package/os/rfc/acceptance.ts +344 -0
- package/os/rfc/decision-log.ts +310 -0
- package/os/rfc/dna-trace.ts +280 -0
- package/os/rfc/frontmatter-io.test.ts +132 -0
- package/os/rfc/frontmatter-io.ts +130 -0
- package/os/rfc/handlers/archive.ts +300 -0
- package/os/rfc/handlers/check.ts +238 -0
- package/os/rfc/handlers/implement-stamp.ts +515 -0
- package/os/rfc/handlers/index-graph.ts +237 -0
- package/os/rfc/handlers/lifecycle.test.ts +127 -0
- package/os/rfc/handlers/lifecycle.ts +249 -0
- package/os/rfc/handlers/list-create.ts +296 -0
- package/os/rfc/handlers/pipeline-status.ts +173 -0
- package/os/rfc/handlers/shared.ts +121 -0
- package/os/rfc/handlers/supersede-propose.ts +241 -0
- package/os/rfc/handlers/validate-rules.test.ts +891 -0
- package/os/rfc/handlers/validate-rules.ts +984 -0
- package/os/rfc/handlers/validate.ts +166 -0
- package/os/rfc/handlers.ts +19 -0
- package/os/rfc/index.ts +97 -0
- package/os/rfc/rfc.module.ts +416 -0
- package/os/rfc/types.ts +596 -0
- package/os/rfc/verification-evidence.ts +244 -0
- package/os/session/atif-parser.ts +166 -0
- package/os/session/frontmatter-io.ts +163 -0
- package/os/session/handlers/archive.ts +288 -0
- package/os/session/handlers/list.ts +174 -0
- package/os/session/handlers/save.ts +348 -0
- package/os/session/handlers/validate.ts +250 -0
- package/os/session/index.ts +50 -0
- package/os/session/session.module.ts +126 -0
- package/os/session/types.ts +203 -0
- package/os/spec/live-spec-list-show-validate.test.ts +257 -0
- package/os/spec/live-spec-list.ts +78 -0
- package/os/spec/live-spec-merge.test.ts +261 -0
- package/os/spec/live-spec-merge.ts +413 -0
- package/os/spec/live-spec-show.ts +117 -0
- package/os/spec/live-spec-types.ts +93 -0
- package/os/spec/live-spec-validate.ts +170 -0
- package/os/spec/spec-materialize.ts +390 -0
- package/os/spec/spec-schema.ts +174 -0
- package/os/spec/spec-status.ts +284 -0
- package/os/spec/spec-validate.ts +513 -0
- package/os/spec/spec.module.ts +135 -0
- package/os/werkstatt/handlers/lock.ts +168 -0
- package/os/werkstatt/handlers/schema.ts +47 -0
- package/os/werkstatt/handlers/werkstatt-lock-recover.ts +175 -0
- package/os/werkstatt/handlers/werkstatt-lock-status.ts +73 -0
- package/os/werkstatt/handlers/werkstatt-operation-validate.ts +104 -0
- package/os/werkstatt/index.ts +39 -0
- package/os/werkstatt/werkstatt.module.ts +62 -0
- package/os/workflow/handlers.ts +304 -0
- package/os/workflow/index.ts +28 -0
- package/os/workflow/types.ts +99 -0
- package/os/workflow/workflow.module.ts +54 -0
- package/package.json +3 -3
- package/profiles/godot-csharp.yaml +4 -4
- package/profiles/knowledge-typescript-turborepo.yaml +110 -0
- package/profiles/phaser-turborepo.yaml +5 -5
- package/skills/_shared/fo-session-summary.md +132 -16
- package/skills/fo/fo-add-tests/pbt-guide.md +13 -13
- package/skills/fo/fo-doc-audit/SKILL.md +6 -6
- package/skills/fo/fo-handoff/SKILL.md +5 -0
- package/skills/fo/fo-idea/SKILL.md +1 -1
- package/skills/fo/fo-review/SKILL.md +1 -1
- package/skills/fo/fo-session-retro/SKILL.md +8 -0
- package/skills/fo/fo-session-save/SKILL.md +8 -0
- package/src/cli-output.ts +59 -0
- package/src/config/__tests__/resolve-terminology.test.ts +45 -0
- package/src/config/forge-config.ts +524 -0
- package/src/forge-module.ts +39 -0
- package/src/index.ts +172 -0
- package/src/knowledge/__tests__/promote.test.ts +219 -0
- package/src/knowledge/__tests__/serialize.test.ts +192 -0
- package/src/knowledge/budgets.ts +202 -0
- package/src/knowledge/compact.ts +405 -0
- package/src/knowledge/index.ts +53 -0
- package/src/knowledge/parse.ts +227 -0
- package/src/knowledge/promote.ts +156 -0
- package/src/knowledge/schema.ts +112 -0
- package/src/knowledge/serialize.ts +72 -0
- package/src/migration-adapters/__tests__/ignored-files.test.ts +51 -0
- package/src/migration-adapters/__tests__/registry.test.ts +131 -0
- package/src/migration-adapters/__tests__/types.test.ts +86 -0
- package/src/migration-adapters/git-utils.ts +71 -0
- package/src/migration-adapters/ignored-files.ts +221 -0
- package/src/migration-adapters/index.ts +21 -0
- package/src/migration-adapters/node-typescript-pnpm/index.ts +189 -0
- package/src/migration-adapters/phaser-pnpm/index.ts +189 -0
- package/src/migration-adapters/registry.ts +61 -0
- package/src/migration-adapters/types.ts +68 -0
- package/src/onboarding/__tests__/workspace-discovery.test.ts +134 -0
- package/src/onboarding/agents-generate.ts +443 -0
- package/src/onboarding/create.ts +427 -0
- package/src/onboarding/doctor.ts +1297 -0
- package/src/onboarding/init.ts +337 -0
- package/src/onboarding/invariant-engine.ts +448 -0
- package/src/onboarding/memory-scaffold.ts +189 -0
- package/src/onboarding/nested-agents-generate.ts +93 -0
- package/src/onboarding/nested-agents-templates.ts +233 -0
- package/src/onboarding/profile-validate.ts +139 -0
- package/src/onboarding/scaffold-project.ts +299 -0
- package/src/onboarding/scaffold.ts +143 -0
- package/src/onboarding/upgrade.ts +558 -0
- package/src/onboarding/workspace-discovery.ts +167 -0
- package/src/profiles/__tests__/profile-schema.test.ts +228 -0
- package/src/profiles/__tests__/stack-profile.test.ts +190 -0
- package/src/profiles/__tests__/terminology-utils.test.ts +71 -0
- package/src/profiles/profile-schema.ts +350 -0
- package/src/profiles/stack-profile.ts +188 -0
- package/src/profiles/terminology-utils.ts +36 -0
- package/src/registry.ts +185 -0
- package/src/skill-schema.ts +37 -0
- package/src/tests/acceptance-criteria.test.ts +99 -0
- package/src/tests/adr-implement-stamp.test.ts +368 -0
- package/src/tests/agents-generate-domain.test.ts +317 -0
- package/src/tests/agents-generate.test.ts +458 -0
- package/src/tests/bindings-schema.test.ts +314 -0
- package/src/tests/budgets.test.ts +213 -0
- package/src/tests/cli-output.test.ts +196 -0
- package/src/tests/compact.test.ts +796 -0
- package/src/tests/create.test.ts +289 -0
- package/src/tests/doctor-autonomy.test.ts +98 -0
- package/src/tests/doctor-bindings.test.ts +286 -0
- package/src/tests/doctor-domain.test.ts +113 -0
- package/src/tests/exploration-handlers.test.ts +256 -0
- package/src/tests/fixtures/agents-generate-business-before.txt +288 -0
- package/src/tests/forge-config.test.ts +420 -0
- package/src/tests/fs-atomic.test.ts +96 -0
- package/src/tests/fs.test.ts +103 -0
- package/src/tests/generated-marker.test.ts +161 -0
- package/src/tests/hash.test.ts +37 -0
- package/src/tests/implement-stamp.test.ts +505 -0
- package/src/tests/init-bindings.test.ts +183 -0
- package/src/tests/knowledge-parse.test.ts +307 -0
- package/src/tests/knowledge-pbt.test.ts +164 -0
- package/src/tests/memory-scaffold.test.ts +192 -0
- package/src/tests/migration-adapters.test.ts +480 -0
- package/src/tests/package-files.test.ts +54 -0
- package/src/tests/pinned-check.test.ts +194 -0
- package/src/tests/pinned-init.test.ts +126 -0
- package/src/tests/profile-schema.test.ts +194 -0
- package/src/tests/profile-validate.test.ts +120 -0
- package/src/tests/program-lease.test.ts +245 -0
- package/src/tests/program-paths-extended.test.ts +117 -0
- package/src/tests/program-paths.test.ts +98 -0
- package/src/tests/program-property.test.ts +139 -0
- package/src/tests/program-schemas.test.ts +445 -0
- package/src/tests/program-spec-node.test.ts +250 -0
- package/src/tests/promote.test.ts +481 -0
- package/src/tests/registry.test.ts +73 -0
- package/src/tests/scaffold-project.test.ts +150 -0
- package/src/tests/session-handlers.test.ts +503 -0
- package/src/tests/session-pbt.test.ts +190 -0
- package/src/tests/skill-schema.test.ts +146 -0
- package/src/tests/skill-validate-knowledge.test.ts +26 -0
- package/src/tests/skill-validate.test.ts +254 -0
- package/src/tests/stack-profile.test.ts +221 -0
- package/src/tests/string-utils.test.ts +42 -0
- package/src/tests/upgrade.test.ts +428 -0
- package/src/tests/werkstatt-lock.test.ts +343 -0
- package/src/tests/workspace-discovery-domain.test.ts +112 -0
- package/src/tests/workspace-discovery.test.ts +135 -0
- package/src/types.ts +221 -0
- package/src/utils/__tests__/fs-idempotent.test.ts +73 -0
- package/src/utils/__tests__/fs-trash.test.ts +45 -0
- package/src/utils/fs-atomic.ts +93 -0
- package/src/utils/fs-idempotent.ts +41 -0
- package/src/utils/fs-trash-sync.ts +37 -0
- package/src/utils/fs-trash.ts +24 -0
- package/src/utils/fs.ts +74 -0
- package/src/utils/generated-marker.ts +166 -0
- package/src/utils/hash.ts +19 -0
- package/src/utils/index.ts +29 -0
- package/src/utils/string-utils.ts +19 -0
- package/src/validators/__tests__/note-orphan-detect.test.ts +94 -0
- package/src/validators/note-frontmatter-validate.ts +142 -0
- package/src/validators/note-link-validate.ts +145 -0
- package/src/validators/note-orphan-detect.ts +146 -0
- package/src/validators/port-validate.ts +97 -0
- package/src/validators/skill-validate.ts +831 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Canonical forge runtime types — autonomous, no @warpgogol/* dependencies.
|
|
4
|
+
Structurally compatible with @warpgogol/site-kernel types so that forge modules
|
|
5
|
+
can be registered in a Warpgogol kernel without direct dependency.</purpose>
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Do not import from @warpgogol/site-kernel or any @warpgogol/* package.</item>
|
|
8
|
+
<item>Do not add Warpgogol-specific fields to ForgeRuntimeContext — keep it minimal and portable.</item>
|
|
9
|
+
</non-goals>
|
|
10
|
+
</MODULE_CONTRACT>
|
|
11
|
+
<CHANGE_SUMMARY>
|
|
12
|
+
<item>Initial canonical forge types: ForgeCommandInput, ForgeCommandResult, ForgeRuntimeContext, ForgeFlagSpec, Diagnostic, CommandRegistry, ForgeLogger.</item>
|
|
13
|
+
<item>RFC-0518: add GateMetadata, GateSeverity, GatePhase, GateConditional types and optional gate field to ForgeCommandMetadata (structurally compatible with @warpgogol/site-kernel).</item>
|
|
14
|
+
</CHANGE_SUMMARY>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Command input / output
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export type ForgeFlagValue = boolean | string | string[];
|
|
22
|
+
|
|
23
|
+
export interface ForgeCommandInput {
|
|
24
|
+
argv: string[];
|
|
25
|
+
flags: Record<string, ForgeFlagValue>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ForgeCommandTiming {
|
|
29
|
+
durationMs?: number;
|
|
30
|
+
startedAt?: string;
|
|
31
|
+
endedAt?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ForgeNextStep {
|
|
35
|
+
action: string;
|
|
36
|
+
kind: "required" | "optional";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ForgeCommandResult<TData = unknown> {
|
|
40
|
+
data?: TData;
|
|
41
|
+
nextSteps?: ForgeNextStep[];
|
|
42
|
+
exitCode?: number;
|
|
43
|
+
summary?: string;
|
|
44
|
+
timing?: ForgeCommandTiming;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Flag specs (RFC-0260 lineage)
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
export interface ForgeFlagSpec {
|
|
52
|
+
kind: "boolean" | "string" | "string[]";
|
|
53
|
+
required?: boolean;
|
|
54
|
+
default?: ForgeFlagValue;
|
|
55
|
+
description: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Command metadata / definition
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
export type ForgeCommandScope = "app" | "workspace";
|
|
63
|
+
|
|
64
|
+
export interface ForgeCommandMetadata {
|
|
65
|
+
description: string;
|
|
66
|
+
scope: ForgeCommandScope;
|
|
67
|
+
mutatesState?: boolean;
|
|
68
|
+
requiresNetwork?: boolean;
|
|
69
|
+
supportsAllSites?: boolean;
|
|
70
|
+
timeoutMs?: number;
|
|
71
|
+
expectedDurationMs?: number;
|
|
72
|
+
longRunning?: boolean;
|
|
73
|
+
cacheable?: boolean;
|
|
74
|
+
hidden?: boolean;
|
|
75
|
+
/**
|
|
76
|
+
* RFC-0518: declarative gate metadata. Optional. Structurally compatible
|
|
77
|
+
* with GateMetadata from @warpgogol/site-kernel. Does NOT affect execution.
|
|
78
|
+
*/
|
|
79
|
+
gate?: GateMetadata;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// RFC-0518: declarative gate metadata (structurally compatible with @warpgogol/site-kernel)
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
export type GateSeverity = "error" | "warning" | "mixed";
|
|
87
|
+
export type GatePhase = "author" | "postbuild" | "workspace" | "mission" | "release";
|
|
88
|
+
|
|
89
|
+
export interface GateConditional {
|
|
90
|
+
kind: "entitlement" | "flag" | "config";
|
|
91
|
+
ref: string;
|
|
92
|
+
description: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface GateMetadata {
|
|
96
|
+
severity: GateSeverity;
|
|
97
|
+
phase: GatePhase;
|
|
98
|
+
conditional?: GateConditional;
|
|
99
|
+
surfaces?: string[];
|
|
100
|
+
rules?: string[];
|
|
101
|
+
blocks?: string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ForgeRegisteredCommandInfo extends ForgeCommandMetadata {
|
|
105
|
+
name: string;
|
|
106
|
+
provider: "workspace" | "site";
|
|
107
|
+
siteName?: string;
|
|
108
|
+
module?: string;
|
|
109
|
+
flags?: Record<string, ForgeFlagSpec>;
|
|
110
|
+
reads?: string[];
|
|
111
|
+
writes?: string[];
|
|
112
|
+
hidden?: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ForgeCommandDefinition<TData = unknown> extends ForgeCommandMetadata {
|
|
116
|
+
name: string;
|
|
117
|
+
flags?: Record<string, ForgeFlagSpec>;
|
|
118
|
+
reads?: string[];
|
|
119
|
+
writes?: string[];
|
|
120
|
+
execute(
|
|
121
|
+
input: ForgeCommandInput,
|
|
122
|
+
context: ForgeRuntimeContext,
|
|
123
|
+
): Promise<void | ForgeCommandResult<TData>> | void | ForgeCommandResult<TData>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Diagnostic (RFC-0203 lineage)
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
export type DiagnosticSeverity = "error" | "warning" | "info";
|
|
131
|
+
|
|
132
|
+
export interface DiagnosticEvidence {
|
|
133
|
+
kind: "rule" | "rendered" | "source" | "config" | "cache" | "runtime";
|
|
134
|
+
ruleFile?: string;
|
|
135
|
+
ruleId?: string;
|
|
136
|
+
file?: string;
|
|
137
|
+
url?: string;
|
|
138
|
+
snippet?: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface Diagnostic {
|
|
142
|
+
ruleId: string;
|
|
143
|
+
severity: DiagnosticSeverity;
|
|
144
|
+
message: string;
|
|
145
|
+
file?: string;
|
|
146
|
+
line?: number;
|
|
147
|
+
column?: number;
|
|
148
|
+
fixHint?: string;
|
|
149
|
+
evidence?: DiagnosticEvidence[];
|
|
150
|
+
data?: Record<string, unknown>;
|
|
151
|
+
id?: string;
|
|
152
|
+
blockId?: string;
|
|
153
|
+
suggestion?: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface CheckResult {
|
|
157
|
+
command: string;
|
|
158
|
+
status: "pass" | "warn" | "fail";
|
|
159
|
+
diagnostics: Diagnostic[];
|
|
160
|
+
summary: { error: number; warning: number; info: number };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// Logger — minimal, structurally compatible with KernelLogger
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
export interface ForgeLogger {
|
|
168
|
+
section(message: string, details?: unknown): void;
|
|
169
|
+
info(message: string, details?: unknown): void;
|
|
170
|
+
warn(message: string, details?: unknown): void;
|
|
171
|
+
error(message: string, details?: unknown): void;
|
|
172
|
+
success(message: string, details?: unknown): void;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
// CommandRegistry — interface injection for command discovery
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
export interface CommandRegistry {
|
|
180
|
+
listCommandNames(): string[];
|
|
181
|
+
listCommands(): ForgeRegisteredCommandInfo[];
|
|
182
|
+
getCommand(name: string): ForgeCommandDefinition | undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Runtime context — minimal, portable
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
export type ForgeOutputFormat = "pretty" | "json";
|
|
190
|
+
|
|
191
|
+
export interface ForgeSiteContext {
|
|
192
|
+
name: string;
|
|
193
|
+
directory: string;
|
|
194
|
+
toolsDirectory: string;
|
|
195
|
+
configPath?: string;
|
|
196
|
+
packageName?: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface ForgeRuntimeContext {
|
|
200
|
+
workspaceRoot: string;
|
|
201
|
+
logger: ForgeLogger;
|
|
202
|
+
dryRun: boolean;
|
|
203
|
+
outputFormat: ForgeOutputFormat;
|
|
204
|
+
/**
|
|
205
|
+
* Optional command registry — provided by the runner (forge CLI or site-kernel).
|
|
206
|
+
* Forge commands that need to discover other commands (e.g. rfc lifecycle validation)
|
|
207
|
+
* access this through the context instead of importing a kernel-specific function.
|
|
208
|
+
*/
|
|
209
|
+
commandRegistry?: CommandRegistry;
|
|
210
|
+
/** Optional target site/app context — provided when running in app-scoped mode. */
|
|
211
|
+
site?: ForgeSiteContext;
|
|
212
|
+
/** True only when --site <name> was passed explicitly by the caller. */
|
|
213
|
+
siteExplicit?: boolean;
|
|
214
|
+
/**
|
|
215
|
+
* Optional forge root override — provided when a command is called from
|
|
216
|
+
* another command (e.g. forge.create delegates to forge.init and forge.scaffold
|
|
217
|
+
* in a new directory where resolveForgeRoot would fail). When set, handlers
|
|
218
|
+
* use this instead of calling resolveForgeRoot(workspaceRoot).
|
|
219
|
+
*/
|
|
220
|
+
forgeRoot?: string;
|
|
221
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { writeFileIfChanged } from "../fs-idempotent.ts";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { mkdtemp, rm, readFile, writeFile, mkdir } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
let tempDir: string;
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
tempDir = await mkdtemp(join(tmpdir(), "forge-idempotent-"));
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("writeFileIfChanged", () => {
|
|
18
|
+
it("writes new file and returns 'written'", async () => {
|
|
19
|
+
const filePath = join(tempDir, "new.txt");
|
|
20
|
+
const result = await writeFileIfChanged(filePath, "hello");
|
|
21
|
+
expect(result).toBe("written");
|
|
22
|
+
expect(await readFile(filePath, "utf8")).toBe("hello");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("returns 'unchanged' when content is identical", async () => {
|
|
26
|
+
const filePath = join(tempDir, "same.txt");
|
|
27
|
+
await writeFile(filePath, "same content", "utf8");
|
|
28
|
+
const result = await writeFileIfChanged(filePath, "same content");
|
|
29
|
+
expect(result).toBe("unchanged");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("writes when string content differs", async () => {
|
|
33
|
+
const filePath = join(tempDir, "change.txt");
|
|
34
|
+
await writeFile(filePath, "old", "utf8");
|
|
35
|
+
const result = await writeFileIfChanged(filePath, "new");
|
|
36
|
+
expect(result).toBe("written");
|
|
37
|
+
expect(await readFile(filePath, "utf8")).toBe("new");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("writes to nested paths when parent dirs exist", async () => {
|
|
41
|
+
await mkdir(join(tempDir, "sub", "dir"), { recursive: true });
|
|
42
|
+
const filePath = join(tempDir, "sub", "dir", "file.txt");
|
|
43
|
+
const result = await writeFileIfChanged(filePath, "nested");
|
|
44
|
+
expect(result).toBe("written");
|
|
45
|
+
expect(await readFile(filePath, "utf8")).toBe("nested");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("handles Uint8Array content", async () => {
|
|
49
|
+
const filePath = join(tempDir, "binary.bin");
|
|
50
|
+
const data = new Uint8Array([1, 2, 3, 4, 5]);
|
|
51
|
+
const result = await writeFileIfChanged(filePath, data);
|
|
52
|
+
expect(result).toBe("written");
|
|
53
|
+
const buf = await readFile(filePath);
|
|
54
|
+
expect(new Uint8Array(buf)).toEqual(data);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns 'unchanged' for identical Uint8Array content", async () => {
|
|
58
|
+
const filePath = join(tempDir, "binary.bin");
|
|
59
|
+
const data = new Uint8Array([10, 20, 30]);
|
|
60
|
+
await writeFileIfChanged(filePath, data);
|
|
61
|
+
const result = await writeFileIfChanged(filePath, data);
|
|
62
|
+
expect(result).toBe("unchanged");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("writes when Uint8Array content differs", async () => {
|
|
66
|
+
const filePath = join(tempDir, "binary.bin");
|
|
67
|
+
await writeFileIfChanged(filePath, new Uint8Array([1, 2, 3]));
|
|
68
|
+
const result = await writeFileIfChanged(filePath, new Uint8Array([4, 5, 6]));
|
|
69
|
+
expect(result).toBe("written");
|
|
70
|
+
const buf = await readFile(filePath);
|
|
71
|
+
expect(new Uint8Array(buf)).toEqual(new Uint8Array([4, 5, 6]));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { trashPath } from "../fs-trash.ts";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
|
|
8
|
+
let tempDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
tempDir = await mkdtemp(join(tmpdir(), "forge-trash-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("trashPath", () => {
|
|
19
|
+
it("does not throw when path does not exist", async () => {
|
|
20
|
+
const nonExistent = join(tempDir, "nope.txt");
|
|
21
|
+
expect(existsSync(nonExistent)).toBe(false);
|
|
22
|
+
await expect(trashPath(nonExistent)).resolves.toBeUndefined();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("trashes a file", async () => {
|
|
26
|
+
const filePath = join(tempDir, "to-trash.txt");
|
|
27
|
+
await writeFile(filePath, "content", "utf8");
|
|
28
|
+
expect(existsSync(filePath)).toBe(true);
|
|
29
|
+
|
|
30
|
+
await trashPath(filePath);
|
|
31
|
+
|
|
32
|
+
expect(existsSync(filePath)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("trashes a directory", async () => {
|
|
36
|
+
const dirPath = join(tempDir, "to-trash-dir");
|
|
37
|
+
await mkdir(dirPath, { recursive: true });
|
|
38
|
+
await writeFile(join(dirPath, "inner.txt"), "inner", "utf8");
|
|
39
|
+
expect(existsSync(dirPath)).toBe(true);
|
|
40
|
+
|
|
41
|
+
await trashPath(dirPath);
|
|
42
|
+
|
|
43
|
+
expect(existsSync(dirPath)).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Atomic file-write primitive — canonical forge utility. Writes to a temp
|
|
4
|
+
file then renames, with bounded retry for Windows EPERM/EBUSY.</purpose>
|
|
5
|
+
<non-goals>
|
|
6
|
+
<item>Do not provide cross-process locks — convergent atomic writes are sufficient.</item>
|
|
7
|
+
<item>Do not fall back to non-atomic writes — fail loudly.</item>
|
|
8
|
+
</non-goals>
|
|
9
|
+
</MODULE_CONTRACT>
|
|
10
|
+
<CHANGE_SUMMARY>
|
|
11
|
+
<item>Moved from @warpgogol/site-kernel/fs-atomic to forge as canonical source (dependency inversion).</item>
|
|
12
|
+
</CHANGE_SUMMARY>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { writeFile, rename as fsRename, unlink } from "node:fs/promises";
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { dirname, join, basename } from "node:path";
|
|
18
|
+
import { randomBytes } from "node:crypto";
|
|
19
|
+
|
|
20
|
+
export interface WriteFileAtomicOptions {
|
|
21
|
+
retries?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let renameImpl: typeof fsRename = fsRename;
|
|
25
|
+
|
|
26
|
+
export function __setRenameImplForTests(impl: typeof fsRename | undefined): void {
|
|
27
|
+
renameImpl = impl ?? fsRename;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT_RETRIES = 10;
|
|
31
|
+
const RETRY_BACKOFF_MS = 50;
|
|
32
|
+
const RETRYABLE_CODES = new Set(["EPERM", "EBUSY"]);
|
|
33
|
+
|
|
34
|
+
function isRetryableError(error: unknown): boolean {
|
|
35
|
+
if (!error || typeof error !== "object") return false;
|
|
36
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
37
|
+
return typeof code === "string" && RETRYABLE_CODES.has(code);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sleep(ms: number): Promise<void> {
|
|
41
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function makeTempPath(filePath: string): string {
|
|
45
|
+
const dir = dirname(filePath);
|
|
46
|
+
const name = basename(filePath);
|
|
47
|
+
const random = randomBytes(6).toString("hex");
|
|
48
|
+
return join(dir, `${name}.${random}.tmp`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function writeFileAtomic(
|
|
52
|
+
filePath: string,
|
|
53
|
+
content: string | Uint8Array,
|
|
54
|
+
options?: WriteFileAtomicOptions,
|
|
55
|
+
): Promise<void> {
|
|
56
|
+
const retries = options?.retries ?? DEFAULT_RETRIES;
|
|
57
|
+
const tempPath = makeTempPath(filePath);
|
|
58
|
+
let tempWritten = false;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await writeFile(tempPath, content);
|
|
62
|
+
tempWritten = true;
|
|
63
|
+
|
|
64
|
+
let attempt = 0;
|
|
65
|
+
for (;;) {
|
|
66
|
+
try {
|
|
67
|
+
await renameImpl(tempPath, filePath);
|
|
68
|
+
return;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (attempt >= retries || !isRetryableError(error)) {
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
attempt += 1;
|
|
74
|
+
if (existsSync(filePath)) {
|
|
75
|
+
try {
|
|
76
|
+
await unlink(filePath);
|
|
77
|
+
} catch {
|
|
78
|
+
// If unlink also fails, the retry loop will try again.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
await sleep(RETRY_BACKOFF_MS * attempt);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
if (tempWritten) {
|
|
86
|
+
try {
|
|
87
|
+
await unlink(tempPath);
|
|
88
|
+
} catch {
|
|
89
|
+
// Expected in the success path (temp file already renamed away).
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Idempotent file-write primitive — canonical forge utility. Writes only
|
|
4
|
+
when file content differs, delegating to writeFileAtomic for the actual write.
|
|
5
|
+
Reduces unnecessary disk writes and git churn in Compass and Werkstatt commands.</purpose>
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Do not provide cross-process locks — convergent atomic writes are sufficient.</item>
|
|
8
|
+
<item>Do not fall back to non-atomic writes — fail loudly.</item>
|
|
9
|
+
</non-goals>
|
|
10
|
+
</MODULE_CONTRACT>
|
|
11
|
+
<CHANGE_SUMMARY>
|
|
12
|
+
<item>RFC-0556: moved from @warpgogol/site-kernel/fs-idempotent to forge as canonical source (dependency inversion).</item>
|
|
13
|
+
<item>RFC-0603: extended to accept Uint8Array (Buffer) content for idempotent binary file writes — PNG preview images.</item>
|
|
14
|
+
</CHANGE_SUMMARY>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFile } from "node:fs/promises";
|
|
18
|
+
import { writeFileAtomic } from "./fs-atomic.ts";
|
|
19
|
+
|
|
20
|
+
export async function writeFileIfChanged(
|
|
21
|
+
filePath: string,
|
|
22
|
+
content: string | Uint8Array,
|
|
23
|
+
): Promise<"written" | "unchanged"> {
|
|
24
|
+
try {
|
|
25
|
+
if (typeof content === "string") {
|
|
26
|
+
const existing = await readFile(filePath, "utf8");
|
|
27
|
+
if (existing === content) {
|
|
28
|
+
return "unchanged";
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
const existing = await readFile(filePath);
|
|
32
|
+
if (Buffer.compare(existing, Buffer.from(content)) === 0) {
|
|
33
|
+
return "unchanged";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// File does not exist — proceed to write.
|
|
38
|
+
}
|
|
39
|
+
await writeFileAtomic(filePath, content);
|
|
40
|
+
return "written";
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Synchronous trash-can deletion — shells out to `trash-put` (trash-cli)
|
|
4
|
+
on Linux/macOS and PowerShell Recycle Bin on Windows. Used by synchronous
|
|
5
|
+
code paths that cannot use the async `trash` npm package.</purpose>
|
|
6
|
+
<non-goals>
|
|
7
|
+
<item>Do not use for async code paths — use trashPath from fs-trash.ts instead.</item>
|
|
8
|
+
<item>Requires `trash-put` (trash-cli) installed on Linux/macOS. On Windows, uses PowerShell Recycle Bin API (no external binary needed).</item>
|
|
9
|
+
</non-goals>
|
|
10
|
+
</MODULE_CONTRACT>
|
|
11
|
+
<CHANGE_SUMMARY>
|
|
12
|
+
<item>Initial implementation: sync wrapper for `trash-put` (Linux/macOS) and PowerShell Recycle Bin (Windows).</item>
|
|
13
|
+
</CHANGE_SUMMARY>
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, statSync } from "node:fs";
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
18
|
+
|
|
19
|
+
export function trashSync(targetPath: string): void {
|
|
20
|
+
if (!existsSync(targetPath)) return;
|
|
21
|
+
if (process.platform === "win32") {
|
|
22
|
+
const isDir = statSync(targetPath).isDirectory();
|
|
23
|
+
const method = isDir ? "DeleteDirectory" : "DeleteFile";
|
|
24
|
+
const escaped = targetPath.replace(/'/g, "''");
|
|
25
|
+
execFileSync(
|
|
26
|
+
"powershell",
|
|
27
|
+
[
|
|
28
|
+
"-NoProfile",
|
|
29
|
+
"-Command",
|
|
30
|
+
`Add-Type -AssemblyName Microsoft.VisualBasic; [Microsoft.VisualBasic.FileIO.FileSystem]::${method}('${escaped}', 'OnlyErrorDialogs', 'SendToRecycleBin')`,
|
|
31
|
+
],
|
|
32
|
+
{ stdio: "pipe" },
|
|
33
|
+
);
|
|
34
|
+
} else {
|
|
35
|
+
execFileSync("trash-put", [targetPath], { stdio: "pipe" });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Trash-can deletion primitive — moves files/directories to the OS
|
|
4
|
+
trash/recycle bin instead of permanently deleting them. Uses the `trash`
|
|
5
|
+
npm package which implements the FreeDesktop.org Trash specification on
|
|
6
|
+
Linux (no external `trash-put` binary required) and the Recycle Bin on
|
|
7
|
+
Windows.</purpose>
|
|
8
|
+
<non-goals>
|
|
9
|
+
<item>Do not implement trash eviction or retention policies — the OS manages that.</item>
|
|
10
|
+
<item>Do not use for ephemeral cleanup (lock files, temp files, atomic write leftovers) — those are system-internal and should use fs.unlink directly.</item>
|
|
11
|
+
</non-goals>
|
|
12
|
+
</MODULE_CONTRACT>
|
|
13
|
+
<CHANGE_SUMMARY>
|
|
14
|
+
<item>Initial implementation: wraps npm `trash` package with glob disabled for safe path handling.</item>
|
|
15
|
+
</CHANGE_SUMMARY>
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync } from "node:fs";
|
|
19
|
+
import trash from "trash";
|
|
20
|
+
|
|
21
|
+
export async function trashPath(targetPath: string): Promise<void> {
|
|
22
|
+
if (!existsSync(targetPath)) return;
|
|
23
|
+
await trash(targetPath, { glob: false });
|
|
24
|
+
}
|
package/src/utils/fs.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/*
|
|
2
|
+
<MODULE_CONTRACT>
|
|
3
|
+
<purpose>Trivial filesystem utilities — inlined from @warpgogol/share to avoid dependency.</purpose>
|
|
4
|
+
<non-goals>
|
|
5
|
+
<item>Do not add non-filesystem utilities here — use dedicated utility modules.</item>
|
|
6
|
+
<item>Do not introduce @warpgogol/* imports — this package must remain dependency-free.</item>
|
|
7
|
+
</non-goals>
|
|
8
|
+
</MODULE_CONTRACT>
|
|
9
|
+
<CHANGE_SUMMARY>
|
|
10
|
+
<item>Initial inline of collectFiles, fileExists from @warpgogol/werkstatt-shared/share/fs.</item>
|
|
11
|
+
</CHANGE_SUMMARY>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readdir, stat } from "node:fs/promises";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
export interface CollectFilesOptions {
|
|
18
|
+
extensions?: string[];
|
|
19
|
+
ignore?: (name: string) => boolean;
|
|
20
|
+
withDirs?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function defaultIgnore(name: string): boolean {
|
|
24
|
+
return name.startsWith("-") || name.startsWith("old-");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function collectFiles(
|
|
28
|
+
root: string,
|
|
29
|
+
options: CollectFilesOptions = {},
|
|
30
|
+
): Promise<string[]> {
|
|
31
|
+
const { extensions, ignore = defaultIgnore, withDirs = false } = options;
|
|
32
|
+
const results: string[] = [];
|
|
33
|
+
|
|
34
|
+
// fs.walk.lint: allow — this is the canonical collectFiles implementation,
|
|
35
|
+
// inlined from @warpgogol/werkstatt-shared/share/fs to keep @warpgogol/forge dependency-free (RFC-0303).
|
|
36
|
+
async function walk(dir: string): Promise<void> {
|
|
37
|
+
let entries;
|
|
38
|
+
try {
|
|
39
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
40
|
+
} catch {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (ignore(entry.name)) continue;
|
|
46
|
+
|
|
47
|
+
const full = join(dir, entry.name);
|
|
48
|
+
|
|
49
|
+
if (entry.isDirectory()) {
|
|
50
|
+
if (withDirs) results.push(full);
|
|
51
|
+
await walk(full);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!entry.isFile()) continue;
|
|
56
|
+
|
|
57
|
+
if (extensions && !extensions.some((ext) => entry.name.endsWith(ext))) continue;
|
|
58
|
+
|
|
59
|
+
results.push(full);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
await walk(root);
|
|
64
|
+
return results;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function fileExists(path: string): Promise<boolean> {
|
|
68
|
+
try {
|
|
69
|
+
await stat(path);
|
|
70
|
+
return true;
|
|
71
|
+
} catch {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|