@use-the-fork/spindle 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +3 -2
- package/build/Assets/Blueprints/Catalog.js +110 -0
- package/build/Assets/Blueprints/agents/general.agent.md +67 -0
- package/build/Assets/Blueprints/agents/sdd-code-reviewer.agent.md +62 -0
- package/build/Assets/Blueprints/agents/sdd-code-slop-reviewer.agent.md +59 -0
- package/build/Assets/Blueprints/agents/sdd-implementer.agent.md +77 -0
- package/build/Assets/Blueprints/agents/sdd-orchestrator.agent.md +42 -0
- package/build/Assets/Blueprints/agents/sdd-re-review.agent.md +57 -0
- package/build/Assets/Blueprints/agents/sdd-reviewer.agent.md +89 -0
- package/build/Assets/Blueprints/agents/sdd-task-reviewer.agent.md +54 -0
- package/build/Assets/Blueprints/agents/shared-codebase-analyzer.agent.md +151 -0
- package/build/Assets/Blueprints/agents/shared-codebase-locator.agent.md +130 -0
- package/build/Assets/Blueprints/agents/shared-codebase-pattern-finder.agent.md +247 -0
- package/build/Assets/Blueprints/agents/shared-explore.agent.md +53 -0
- package/build/Assets/Blueprints/agents/shared-r.agent.md +17 -0
- package/build/Assets/Blueprints/agents/shared-rw.agent.md +17 -0
- package/build/Assets/Blueprints/agents/shared-rwx.agent.md +17 -0
- package/build/Assets/Blueprints/agents/shared-web-search-researcher.agent.md +122 -0
- package/build/Assets/Blueprints/instructions/code-commenting.instructions.md +39 -0
- package/build/Assets/Blueprints/instructions/source-architecture.instructions.md +15 -0
- package/build/Assets/Blueprints/skills/asd-ste100/CREDITS.MD +1 -0
- package/build/Assets/Blueprints/skills/asd-ste100/SKILL.md +83 -0
- package/build/Assets/Blueprints/skills/asd-ste100/examples/before-after.md +107 -0
- package/build/Assets/Blueprints/skills/asd-ste100/references/writing-rules.md +65 -0
- package/build/Assets/Blueprints/skills/author-agent/SKILL.md +26 -0
- package/build/Assets/Blueprints/skills/author-design/SKILL.md +38 -0
- package/build/Assets/Blueprints/skills/author-instruction/SKILL.md +28 -0
- package/build/Assets/Blueprints/skills/author-skill/SKILL.md +26 -0
- package/build/Assets/Blueprints/skills/grill/SKILL.md +33 -0
- package/build/Assets/Blueprints/skills/grill-design/SKILL.md +41 -0
- package/build/Assets/Blueprints/skills/grill-with-docs/SKILL.md +14 -0
- package/build/Assets/Blueprints/skills/implement/SKILL.md +79 -0
- package/build/Assets/Blueprints/skills/implement-spec/SKILL.md +34 -0
- package/build/Assets/Blueprints/skills/improve-codebase-architecture/HTML-REPORT.md +129 -0
- package/build/Assets/Blueprints/skills/improve-codebase-architecture/SKILL.md +74 -0
- package/build/Assets/Blueprints/skills/int-code-review/SKILL.md +76 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/SKILL.md +238 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/_sections.md +36 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/_template.md +42 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/comments-closing-brace-labels.md +112 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/comments-empty-docblocks.md +132 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/comments-narration.md +118 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/comments-placeholder.md +130 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/defensive-generic-catch.md +153 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/defensive-impossible-null.md +157 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/defensive-missing-real.md +157 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/naming-generic-placeholders.md +120 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/naming-over-descriptive.md +108 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/naming-suffix-abuse.md +167 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/naming-type-in-name.md +100 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/over-eng-dependency-creep.md +114 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/over-eng-premature-interface.md +136 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/over-eng-single-method-class.md +161 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/over-eng-useless-wrapper.md +140 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/style-as-any-escape.md +127 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/style-debug-artifacts.md +144 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/style-hyper-consistent.md +84 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/style-no-hack-scars.md +113 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/style-trivial-boilerplate.md +191 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/test-doesnt-throw.md +132 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/test-mirror-implementation.md +125 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/test-mock-everything.md +148 -0
- package/build/Assets/Blueprints/skills/int-code-slop-review/rules/test-snapshot-abuse.md +131 -0
- package/build/Assets/Blueprints/skills/int-codebase-design/SKILL.md +116 -0
- package/build/Assets/Blueprints/skills/int-codebase-design/references/DEEPENING.md +37 -0
- package/build/Assets/Blueprints/skills/int-codebase-design/references/DESIGN-IT-TWICE.md +44 -0
- package/build/Assets/Blueprints/skills/int-domain-modeling/SKILL.md +78 -0
- package/build/Assets/Blueprints/skills/int-domain-modeling/references/ADR-FORMAT.md +47 -0
- package/build/Assets/Blueprints/skills/int-domain-modeling/references/GLOSSARY-FORMAT.md +60 -0
- package/build/Assets/Blueprints/skills/int-prototype/LOGIC.md +67 -0
- package/build/Assets/Blueprints/skills/int-prototype/SKILL.md +30 -0
- package/build/Assets/Blueprints/skills/int-prototype/UI.md +112 -0
- package/build/Assets/Blueprints/skills/int-retrospective/SKILL.md +88 -0
- package/build/Assets/Blueprints/skills/int-retrospective/assets/retrospective-prompt.md +38 -0
- package/build/Assets/Blueprints/skills/int-retrospective/references/assessment.md +56 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/SKILL.md +64 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/assets/implementer-prompt.md +40 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/assets/reviewer-prompt.md +43 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/aggregate-review.md +7 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/corrective-rounds.md +72 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/example-workflow.md +25 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/full-spec.md +72 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/implementer-corrections.md +9 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/review-inputs.md +26 -0
- package/build/Assets/Blueprints/skills/int-subagent-driven-dev/references/verification-evidence.md +7 -0
- package/build/Assets/Blueprints/skills/int-test-driven-development/SKILL.md +51 -0
- package/build/Assets/Blueprints/skills/int-test-driven-development/references/mocking.md +31 -0
- package/build/Assets/Blueprints/skills/int-test-driven-development/references/tests.md +68 -0
- package/build/Assets/Blueprints/skills/int-testing-agent-guidance/SKILL.md +53 -0
- package/build/Assets/Blueprints/skills/int-verification-before-completion/SKILL.md +25 -0
- package/build/Assets/Blueprints/skills/int-writing-agents/SKILL.md +66 -0
- package/build/Assets/Blueprints/skills/int-writing-for-agents/SKILL.md +86 -0
- package/build/Assets/Blueprints/skills/int-writing-instruction/SKILL.md +57 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/SKILL.md +129 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/assets/interactive.md +83 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/assets/reference.md +32 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/references/arguments.md +50 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/references/interactive-skills.md +53 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/references/persuasion-principles.md +220 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/references/reference-skills.md +20 -0
- package/build/Assets/Blueprints/skills/int-writing-skills/references/testing-skills.md +48 -0
- package/build/Assets/Blueprints/skills/retrospective/SKILL.md +16 -0
- package/build/Assets/Blueprints/skills/setup-project/SKILL.md +66 -0
- package/build/Assets/Blueprints/skills/to-spec/SKILL.md +53 -0
- package/build/Assets/Blueprints/skills/to-tickets/SKILL.md +49 -0
- package/build/Assets/Blueprints/skills/update-tooling/SKILL.md +86 -0
- package/build/Assets/Templates/Catalog.js +12 -0
- package/build/Assets/Templates/Catalog.ts +17 -0
- package/build/Assets/Templates/project/copilot-instructions.md +17 -0
- package/build/Assets/Templates/project/domain.md +51 -0
- package/build/Assets/Templates/project/ticket-tracker.md +33 -0
- package/build/Assets/Templates/sdd/design.md +25 -0
- package/build/Assets/Templates/sdd/spec.md +29 -0
- package/build/Assets/Templates/sdd/ticket.md +15 -0
- package/build/BlueprintManagement/BlueprintManagementServiceProvider.js +15 -0
- package/build/BlueprintManagement/Catalog/BlueprintCatalog.js +238 -0
- package/build/BlueprintManagement/Commands/BlueprintCommandOptions.js +9 -0
- package/build/BlueprintManagement/Commands/ConflictPolicy.js +6 -0
- package/build/BlueprintManagement/Commands/InitCommand.js +117 -0
- package/build/BlueprintManagement/Commands/Prompts.js +66 -0
- package/build/BlueprintManagement/Commands/UpdateCommand.js +86 -0
- package/build/BlueprintManagement/Handlers/Agents/AgentHandler.js +21 -0
- package/build/BlueprintManagement/Handlers/BlueprintHandlers.js +4 -0
- package/build/BlueprintManagement/Handlers/Instructions/InstructionHandler.js +21 -0
- package/build/BlueprintManagement/Handlers/Skills/SkillHandler.js +10 -0
- package/build/BlueprintManagement/Reconciliation/BlueprintCatalogPlanner.js +87 -0
- package/build/BlueprintManagement/Reconciliation/BlueprintRemovalCoordinator.js +78 -0
- package/build/BlueprintManagement/Services/BlueprintReconciliationService.js +391 -0
- package/build/BlueprintManagement/Services/InstallationService.js +54 -0
- package/build/BlueprintManagement/Support/BlueprintFiles.js +113 -0
- package/build/BlueprintManagement/Types/Blueprint.js +1 -0
- package/build/BlueprintManagement/Types/Installation.js +1 -0
- package/build/BlueprintManagement/Types/Reconciliation.js +1 -0
- package/build/BlueprintManagement/Versioning/BlueprintVersion.js +70 -0
- package/build/BlueprintManagement/Versioning/BlueprintVersioning.js +55 -0
- package/build/Commands/Command.js +3 -0
- package/build/Commands/CommandOptions.js +12 -0
- package/build/Config/Config.js +3 -0
- package/build/Foundation/Application.js +112 -0
- package/build/Foundation/ApplicationBuilder.js +23 -0
- package/build/Foundation/Bootstrap/Bootstrapper.js +2 -0
- package/build/Foundation/Bootstrap/DiscoverGitRoot.js +8 -0
- package/build/Foundation/Bootstrap/LoadConsoleArgs.js +47 -0
- package/build/Foundation/Bootstrap/LoadProjectConfiguration.js +144 -0
- package/build/Foundation/Bootstrap/RegisterProviders.js +12 -0
- package/build/Foundation/Bootstrap/RequireProjectConfiguration.js +14 -0
- package/build/Foundation/ConsoleInvocation.js +1 -0
- package/build/Foundation/Container.js +185 -0
- package/build/Foundation/FoundationServiceProvider.js +5 -0
- package/build/Foundation/Kernel.js +30 -0
- package/build/Foundation/Service.js +23 -0
- package/build/Foundation/ServiceProvider.js +12 -0
- package/build/Git/GitServiceProvider.js +8 -0
- package/build/Git/Services/GitService.js +26 -0
- package/build/Project/ProjectServiceProvider.js +8 -0
- package/build/Project/Services/ProjectService.js +124 -0
- package/build/SubagentDrivenDevelopment/Commands/ChecksRecordCommand.js +35 -0
- package/build/SubagentDrivenDevelopment/Commands/ChecksRequireCommand.js +33 -0
- package/build/SubagentDrivenDevelopment/Commands/ClaimCommand.js +23 -0
- package/build/SubagentDrivenDevelopment/Commands/CorrectCommand.js +37 -0
- package/build/SubagentDrivenDevelopment/Commands/CreateCommand.js +26 -0
- package/build/SubagentDrivenDevelopment/Commands/FindingCreateCommand.js +39 -0
- package/build/SubagentDrivenDevelopment/Commands/FindingUpdateCommand.js +36 -0
- package/build/SubagentDrivenDevelopment/Commands/FinishCommand.js +23 -0
- package/build/SubagentDrivenDevelopment/Commands/ImplementCommand.js +23 -0
- package/build/SubagentDrivenDevelopment/Commands/NextCommand.js +21 -0
- package/build/SubagentDrivenDevelopment/Commands/ReadyCommand.js +21 -0
- package/build/SubagentDrivenDevelopment/Commands/RequirementsDecideCommand.js +33 -0
- package/build/SubagentDrivenDevelopment/Commands/ResumeCommand.js +31 -0
- package/build/SubagentDrivenDevelopment/Commands/ReviewCommand.js +55 -0
- package/build/SubagentDrivenDevelopment/Commands/ReviewRequireCommand.js +12 -0
- package/build/SubagentDrivenDevelopment/Commands/StatusCommand.js +21 -0
- package/build/SubagentDrivenDevelopment/Commands/SubmitCommand.js +59 -0
- package/build/SubagentDrivenDevelopment/Commands/TicketCommentCommand.js +24 -0
- package/build/SubagentDrivenDevelopment/Commands/TicketCreateCommand.js +25 -0
- package/build/SubagentDrivenDevelopment/Commands/TicketUpdateCommand.js +31 -0
- package/build/SubagentDrivenDevelopment/Commands/groups.js +36 -0
- package/build/SubagentDrivenDevelopment/Evidence/GitEvidence.js +449 -0
- package/build/SubagentDrivenDevelopment/Evidence/ReportEvidence.js +80 -0
- package/build/SubagentDrivenDevelopment/Models/Assignments.js +1 -0
- package/build/SubagentDrivenDevelopment/Models/Evidence.js +1 -0
- package/build/SubagentDrivenDevelopment/Models/Findings.js +1 -0
- package/build/SubagentDrivenDevelopment/Models/Options.js +1 -0
- package/build/SubagentDrivenDevelopment/Models/Workflow.js +16 -0
- package/build/SubagentDrivenDevelopment/Persistence/AssignmentSchema.js +155 -0
- package/build/SubagentDrivenDevelopment/Persistence/CheckSchema.js +71 -0
- package/build/SubagentDrivenDevelopment/Persistence/FeatureWorkspace.js +76 -0
- package/build/SubagentDrivenDevelopment/Persistence/FileSystem.js +28 -0
- package/build/SubagentDrivenDevelopment/Persistence/FindingSchema.js +81 -0
- package/build/SubagentDrivenDevelopment/Persistence/LedgerRepository.js +322 -0
- package/build/SubagentDrivenDevelopment/Persistence/PathSafety.js +62 -0
- package/build/SubagentDrivenDevelopment/Persistence/ProgressSchema.js +95 -0
- package/build/SubagentDrivenDevelopment/Persistence/ReadinessReader.js +40 -0
- package/build/SubagentDrivenDevelopment/Persistence/ReviewSchema.js +68 -0
- package/build/SubagentDrivenDevelopment/Persistence/TicketRepository.js +147 -0
- package/build/SubagentDrivenDevelopment/Persistence/WorkspaceTransaction.js +282 -0
- package/build/SubagentDrivenDevelopment/Policies/AcceptancePolicy.js +100 -0
- package/build/SubagentDrivenDevelopment/Policies/AssignmentPolicy.js +36 -0
- package/build/SubagentDrivenDevelopment/Policies/CheckPolicy.js +93 -0
- package/build/SubagentDrivenDevelopment/Policies/FindingPolicy.js +53 -0
- package/build/SubagentDrivenDevelopment/Policies/InputValidation.js +36 -0
- package/build/SubagentDrivenDevelopment/Policies/ReadinessPolicy.js +26 -0
- package/build/SubagentDrivenDevelopment/Policies/RequiredReviewers.js +12 -0
- package/build/SubagentDrivenDevelopment/Policies/ReviewPolicy.js +87 -0
- package/build/SubagentDrivenDevelopment/Policies/WorkflowPolicy.js +85 -0
- package/build/SubagentDrivenDevelopment/Services/AssignmentService.js +269 -0
- package/build/SubagentDrivenDevelopment/Services/ChecksService.js +319 -0
- package/build/SubagentDrivenDevelopment/Services/CorrectionService.js +273 -0
- package/build/SubagentDrivenDevelopment/Services/DeliveryCorrectionService.js +253 -0
- package/build/SubagentDrivenDevelopment/Services/DeliveryFindingService.js +302 -0
- package/build/SubagentDrivenDevelopment/Services/DeliveryService.js +160 -0
- package/build/SubagentDrivenDevelopment/Services/DeliverySubmissionService.js +273 -0
- package/build/SubagentDrivenDevelopment/Services/FeatureCreationService.js +138 -0
- package/build/SubagentDrivenDevelopment/Services/FeatureInspectionService.js +399 -0
- package/build/SubagentDrivenDevelopment/Services/FindingService.js +392 -0
- package/build/SubagentDrivenDevelopment/Services/IntegrationService.js +258 -0
- package/build/SubagentDrivenDevelopment/Services/ReadinessService.js +21 -0
- package/build/SubagentDrivenDevelopment/Services/RequirementsService.js +216 -0
- package/build/SubagentDrivenDevelopment/Services/ReviewService.js +471 -0
- package/build/SubagentDrivenDevelopment/Services/SubmissionService.js +299 -0
- package/build/SubagentDrivenDevelopment/Services/TicketService.js +186 -0
- package/build/SubagentDrivenDevelopment/SubagentDrivenDevelopmentServiceProvider.js +113 -0
- package/build/Support/Records.js +5 -0
- package/build/Support/Repository.js +139 -0
- package/build/Support/Stacks.js +10 -0
- package/build/TemplateRendering/Computed/ComputedContextService.js +13 -0
- package/build/TemplateRendering/Computed/ComputedVariableProvider.js +6 -0
- package/build/TemplateRendering/Computed/ComputedVariableProviders.js +6 -0
- package/build/TemplateRendering/Computed/Providers/InternalSkillDescriptionProvider.js +6 -0
- package/build/TemplateRendering/Computed/Providers/SpindlePackageManagerProvider.js +11 -0
- package/build/TemplateRendering/Computed/ResolveRenderContext.js +18 -0
- package/build/TemplateRendering/Rendering/RenderText.js +43 -0
- package/build/TemplateRendering/TemplateRenderingServiceProvider.js +8 -0
- package/build/Tooling/Commands/ToolingAddCommand.js +29 -0
- package/build/Tooling/Commands/ToolingListCommand.js +19 -0
- package/build/Tooling/Commands/ToolingRemoveCommand.js +19 -0
- package/build/Tooling/Commands/ToolingUpdateCommand.js +30 -0
- package/build/Tooling/Commands/groups.js +4 -0
- package/build/Tooling/Models/ToolingEntry.js +1 -0
- package/build/Tooling/Services/ToolingService.js +278 -0
- package/build/Tooling/ToolingServiceProvider.js +15 -0
- package/build/cli.js +36 -0
- package/build/index.js +31 -0
- package/package.json +54 -3
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, relative, sep } from "node:path";
|
|
3
|
+
import { planBlueprintCatalogReconciliation } from "../Reconciliation/BlueprintCatalogPlanner.js";
|
|
4
|
+
import { applyRemovalOperations, catalogRemovalOperation, membershipRemovalOperation, validateRemovalTarget, } from "../Reconciliation/BlueprintRemovalCoordinator.js";
|
|
5
|
+
import { InstallationService } from "./InstallationService.js";
|
|
6
|
+
import { assertSafeInstallPaths, assertSingleFileDestination, exists, preparePlannedBlueprintFiles, } from "../Support/BlueprintFiles.js";
|
|
7
|
+
import { readBlueprintVersion } from "../Versioning/BlueprintVersion.js";
|
|
8
|
+
import { LoadProjectConfiguration } from "../../Foundation/Bootstrap/LoadProjectConfiguration.js";
|
|
9
|
+
import { Service } from "../../Foundation/Service.js";
|
|
10
|
+
import { ComputedContextService } from "../../TemplateRendering/Computed/ComputedContextService.js";
|
|
11
|
+
function willInstallBlueprint(blueprint, selectedNewBlueprints, decisions) {
|
|
12
|
+
return (blueprint.status === "install" ||
|
|
13
|
+
(blueprint.status === "new" && selectedNewBlueprints.has(blueprint.name)) ||
|
|
14
|
+
((blueprint.status === "conflict" || blueprint.status === "update") &&
|
|
15
|
+
decisions.get(blueprint.name) === "overwrite"));
|
|
16
|
+
}
|
|
17
|
+
async function prepareFiles(files, context, handler, renderedEntry) {
|
|
18
|
+
const requireText = handler.entryFile !== undefined
|
|
19
|
+
? true
|
|
20
|
+
: handler.fileTarget
|
|
21
|
+
? (file) => file.outputPath.toLowerCase().endsWith(".md")
|
|
22
|
+
: false;
|
|
23
|
+
if (renderedEntry === undefined) {
|
|
24
|
+
return preparePlannedBlueprintFiles(files, context, handler.validateMetadata, requireText);
|
|
25
|
+
}
|
|
26
|
+
const prepared = [];
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
if (file.sourcePath !== renderedEntry.sourcePath ||
|
|
29
|
+
file.outputPath !== renderedEntry.outputPath) {
|
|
30
|
+
prepared.push(...(await preparePlannedBlueprintFiles([file], context, handler.validateMetadata, requireText)));
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const bytes = await readFile(file.sourcePath);
|
|
34
|
+
if (!Buffer.from(renderedEntry.sourceText, "utf8").equals(bytes)) {
|
|
35
|
+
throw new Error(`Blueprint entry '${file.outputPath}' changed while planning; run again.`);
|
|
36
|
+
}
|
|
37
|
+
if (bytes.includes(0)) {
|
|
38
|
+
prepared.push(...(await preparePlannedBlueprintFiles([file], context, handler.validateMetadata, requireText)));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
handler.validateMetadata?.(file.outputPath, renderedEntry.renderedText);
|
|
42
|
+
prepared.push({ ...file, renderedText: renderedEntry.renderedText });
|
|
43
|
+
}
|
|
44
|
+
return prepared;
|
|
45
|
+
}
|
|
46
|
+
async function planReconciliation(handler, options, blueprintNames, sourceFilesByName, resolveContext) {
|
|
47
|
+
const { mode, githubDirectory } = options;
|
|
48
|
+
const destinationDirectory = handler.destinationDirectory(githubDirectory);
|
|
49
|
+
const targetPath = (name) => handler.targetPath?.(destinationDirectory, name) ?? join(destinationDirectory, name);
|
|
50
|
+
const plan = [];
|
|
51
|
+
for (const name of blueprintNames) {
|
|
52
|
+
const target = targetPath(name);
|
|
53
|
+
const installed = await exists(target);
|
|
54
|
+
if (mode === "init") {
|
|
55
|
+
plan.push({ name, status: installed ? "conflict" : "install" });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
plan.push({
|
|
59
|
+
name,
|
|
60
|
+
...(handler.planUpdate
|
|
61
|
+
? await handler.planUpdate({
|
|
62
|
+
name,
|
|
63
|
+
sourceFiles: sourceFilesByName.get(name),
|
|
64
|
+
target,
|
|
65
|
+
installed,
|
|
66
|
+
resolveContext,
|
|
67
|
+
})
|
|
68
|
+
: { status: installed ? "conflict" : "new" }),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const newBlueprints = plan.filter((blueprint) => blueprint.status === "new").map((b) => b.name);
|
|
72
|
+
const selectedNewBlueprints = new Set(options.mode === "update"
|
|
73
|
+
? options.includeNew
|
|
74
|
+
? newBlueprints
|
|
75
|
+
: options.chooseNew && newBlueprints.length > 0
|
|
76
|
+
? await options.chooseNew(handler.kind, newBlueprints)
|
|
77
|
+
: []
|
|
78
|
+
: []);
|
|
79
|
+
const decisions = new Map();
|
|
80
|
+
for (const blueprint of plan) {
|
|
81
|
+
if (blueprint.status !== "conflict" && blueprint.status !== "update")
|
|
82
|
+
continue;
|
|
83
|
+
if (options.conflictPolicy !== undefined) {
|
|
84
|
+
decisions.set(blueprint.name, options.conflictPolicy);
|
|
85
|
+
}
|
|
86
|
+
else if (options.chooseConflict) {
|
|
87
|
+
decisions.set(blueprint.name, await options.chooseConflict(handler.kind, blueprint.name, blueprint.installedVersion, blueprint.availableVersion));
|
|
88
|
+
}
|
|
89
|
+
else if (blueprint.status === "conflict") {
|
|
90
|
+
throw new Error(`${handler.kind} '${blueprint.name}' already exists; pass --on-conflict <overwrite|skip> to choose what to do in non-interactive runs.`);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
throw new Error(`${handler.kind} '${blueprint.name}' is outdated (${blueprint.installedVersion} -> ${blueprint.availableVersion}); pass --on-conflict <overwrite|skip> to choose what to do in non-interactive runs.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const willInstall = plan.some((blueprint) => willInstallBlueprint(blueprint, selectedNewBlueprints, decisions));
|
|
97
|
+
return {
|
|
98
|
+
handler,
|
|
99
|
+
mode,
|
|
100
|
+
githubDirectory,
|
|
101
|
+
destinationDirectory,
|
|
102
|
+
sourceFilesByName,
|
|
103
|
+
plan,
|
|
104
|
+
selectedNewBlueprints,
|
|
105
|
+
decisions,
|
|
106
|
+
willInstall,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
async function applyReconciliation(prepared, preparedFilesByName, membershipCleanupOperations, installationService, recordAppliedChange) {
|
|
110
|
+
const { handler, mode, githubDirectory, destinationDirectory, plan, selectedNewBlueprints, decisions, } = prepared;
|
|
111
|
+
const targetPath = (name) => handler.targetPath?.(destinationDirectory, name) ?? join(destinationDirectory, name);
|
|
112
|
+
const outcomes = [];
|
|
113
|
+
for (const blueprint of plan) {
|
|
114
|
+
if (blueprint.status === "warning") {
|
|
115
|
+
outcomes.push({
|
|
116
|
+
kind: handler.kind,
|
|
117
|
+
name: blueprint.name,
|
|
118
|
+
status: "skipped",
|
|
119
|
+
warning: blueprint.warning,
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (blueprint.status === "up-to-date") {
|
|
124
|
+
outcomes.push({ kind: handler.kind, name: blueprint.name, status: "up-to-date" });
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (blueprint.status === "new" && !selectedNewBlueprints.has(blueprint.name)) {
|
|
128
|
+
outcomes.push({ kind: handler.kind, name: blueprint.name, status: "skipped" });
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if ((blueprint.status === "conflict" || blueprint.status === "update") &&
|
|
132
|
+
decisions.get(blueprint.name) === "skip") {
|
|
133
|
+
outcomes.push({ kind: handler.kind, name: blueprint.name, status: "skipped" });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const target = targetPath(blueprint.name);
|
|
137
|
+
await assertSafeInstallPaths(githubDirectory, [blueprint.name], destinationDirectory, targetPath);
|
|
138
|
+
const replacing = blueprint.status === "conflict" || blueprint.status === "update";
|
|
139
|
+
const replacedMembershipPaths = [];
|
|
140
|
+
try {
|
|
141
|
+
if (replacing && handler.entryFile === undefined && !handler.fileTarget) {
|
|
142
|
+
for (const operation of membershipCleanupOperations) {
|
|
143
|
+
if (operation.handler !== handler)
|
|
144
|
+
continue;
|
|
145
|
+
const pathWithinTarget = relative(target, operation.plan.target);
|
|
146
|
+
if (pathWithinTarget === "" ||
|
|
147
|
+
pathWithinTarget === ".." ||
|
|
148
|
+
pathWithinTarget.startsWith(`..${sep}`)) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (await validateRemovalTarget(githubDirectory, {
|
|
152
|
+
...operation,
|
|
153
|
+
expectedTarget: "file",
|
|
154
|
+
})) {
|
|
155
|
+
replacedMembershipPaths.push(operation.plan);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
await rm(target, { recursive: true, force: true });
|
|
159
|
+
recordAppliedChange(`removed existing ${handler.kind} '${blueprint.name}' before replacement`);
|
|
160
|
+
for (const removed of replacedMembershipPaths) {
|
|
161
|
+
recordAppliedChange(`removed destination-only ${handler.kind} file '${removed.path}'`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
outcomes.push(...replacedMembershipPaths.map((removed) => ({
|
|
165
|
+
kind: handler.kind,
|
|
166
|
+
name: removed.path,
|
|
167
|
+
status: "removed",
|
|
168
|
+
})));
|
|
169
|
+
await mkdir(destinationDirectory, { recursive: true });
|
|
170
|
+
const preparedFiles = preparedFilesByName.get(blueprint.name);
|
|
171
|
+
if (preparedFiles === undefined) {
|
|
172
|
+
throw new Error(`No prepared files are available for ${handler.kind} '${blueprint.name}'.`);
|
|
173
|
+
}
|
|
174
|
+
await installationService.installPreparedFiles(preparedFiles, target, handler.entryFile, replacing && (handler.entryFile !== undefined || handler.fileTarget === true), handler.fileTarget, (file) => {
|
|
175
|
+
if (handler.kind === "Skill") {
|
|
176
|
+
recordAppliedChange(`applied Skill file '${blueprint.name}/${file.outputPath}'`);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
const status = mode === "init" || blueprint.status === "new" ? "installed" : "updated";
|
|
180
|
+
recordAppliedChange(`${status} ${handler.kind} '${blueprint.name}'`);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
throw new Error(`Failed to apply ${handler.kind} '${blueprint.name}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
184
|
+
}
|
|
185
|
+
outcomes.push({
|
|
186
|
+
kind: handler.kind,
|
|
187
|
+
name: blueprint.name,
|
|
188
|
+
status: mode === "init" || blueprint.status === "new" ? "installed" : "updated",
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return outcomes;
|
|
192
|
+
}
|
|
193
|
+
async function performReconciliation(handlers, options, installationService) {
|
|
194
|
+
const outcomes = [];
|
|
195
|
+
let hasBlueprints = false;
|
|
196
|
+
const stacks = options.config?.stacks ?? options.context?.stacks ?? [];
|
|
197
|
+
const collections = [];
|
|
198
|
+
const initializers = [];
|
|
199
|
+
const deletionOperations = [];
|
|
200
|
+
const membershipCleanupOperations = [];
|
|
201
|
+
await assertSafeInstallPaths(options.githubDirectory, [], options.githubDirectory);
|
|
202
|
+
const handlerPlans = await planBlueprintCatalogReconciliation(handlers, options.githubDirectory, options.blueprintSourceRoot, stacks);
|
|
203
|
+
for (const handlerPlan of handlerPlans) {
|
|
204
|
+
const { handler, selections, catalogDeletions } = handlerPlan;
|
|
205
|
+
const destinationDirectory = handler.destinationDirectory(options.githubDirectory);
|
|
206
|
+
const deletions = handlerPlan.deletions;
|
|
207
|
+
membershipCleanupOperations.push(...handlerPlan.membershipCleanup.map((plan) => ({ handler, plan })));
|
|
208
|
+
catalogDeletions.forEach((deletion, index) => {
|
|
209
|
+
deletionOperations.push({ handler, deletion, plan: deletions[index] });
|
|
210
|
+
});
|
|
211
|
+
if (deletions.length > 0 || handlerPlan.membershipCleanup.length > 0)
|
|
212
|
+
hasBlueprints = true;
|
|
213
|
+
if (handler.prepareInitialize !== undefined) {
|
|
214
|
+
if (selections.length > 0 || catalogDeletions.length > 0) {
|
|
215
|
+
hasBlueprints = true;
|
|
216
|
+
initializers.push({ handler, selections, deletions: catalogDeletions });
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const files = new Map();
|
|
221
|
+
for (const selection of selections) {
|
|
222
|
+
files.set(selection.name, selection.files);
|
|
223
|
+
}
|
|
224
|
+
const names = [...files.keys()];
|
|
225
|
+
if (names.length === 0)
|
|
226
|
+
continue;
|
|
227
|
+
hasBlueprints = true;
|
|
228
|
+
const targetPath = (name) => handler.targetPath?.(destinationDirectory, name) ?? join(destinationDirectory, name);
|
|
229
|
+
await assertSafeInstallPaths(options.githubDirectory, names, destinationDirectory, targetPath);
|
|
230
|
+
if (handler.entryFile !== undefined || handler.fileTarget) {
|
|
231
|
+
for (const name of names)
|
|
232
|
+
await assertSingleFileDestination(targetPath(name));
|
|
233
|
+
}
|
|
234
|
+
collections.push({ handler, names, files, deletions });
|
|
235
|
+
}
|
|
236
|
+
const plannedRemovals = [
|
|
237
|
+
...deletionOperations.map(catalogRemovalOperation),
|
|
238
|
+
...membershipCleanupOperations.map(membershipRemovalOperation),
|
|
239
|
+
];
|
|
240
|
+
for (const operation of plannedRemovals) {
|
|
241
|
+
await validateRemovalTarget(options.githubDirectory, operation);
|
|
242
|
+
}
|
|
243
|
+
if (!hasBlueprints)
|
|
244
|
+
throw new Error("No Blueprints are available to install.");
|
|
245
|
+
let contextService;
|
|
246
|
+
const resolveContext = async () => {
|
|
247
|
+
contextService ??= options.app.make(ComputedContextService);
|
|
248
|
+
return (await contextService).resolve(async () => options.context ?? (await options.resolveContext?.()));
|
|
249
|
+
};
|
|
250
|
+
const plans = [];
|
|
251
|
+
for (const { handler, names, files } of collections) {
|
|
252
|
+
plans.push(await planReconciliation(handler, options, names, files, resolveContext));
|
|
253
|
+
}
|
|
254
|
+
const installing = plans.find((plan) => plan.willInstall);
|
|
255
|
+
const needsContext = installing !== undefined ||
|
|
256
|
+
initializers.some((initializer) => initializer.selections.length > 0);
|
|
257
|
+
const context = needsContext ? await resolveContext() : options.context;
|
|
258
|
+
if (needsContext && !context) {
|
|
259
|
+
throw new Error("Cannot install Blueprints without resolved Variables.");
|
|
260
|
+
}
|
|
261
|
+
for (const plan of plans) {
|
|
262
|
+
const targetPath = (name) => plan.handler.targetPath?.(plan.destinationDirectory, name) ??
|
|
263
|
+
join(plan.destinationDirectory, name);
|
|
264
|
+
await assertSafeInstallPaths(plan.githubDirectory, plan.plan.map((blueprint) => blueprint.name), plan.destinationDirectory, targetPath);
|
|
265
|
+
for (const blueprint of plan.plan) {
|
|
266
|
+
if (plan.handler.entryFile !== undefined || plan.handler.fileTarget) {
|
|
267
|
+
await assertSingleFileDestination(targetPath(blueprint.name));
|
|
268
|
+
}
|
|
269
|
+
const selectedNew = blueprint.status === "install" ||
|
|
270
|
+
(blueprint.status === "new" && plan.selectedNewBlueprints.has(blueprint.name));
|
|
271
|
+
if (selectedNew && (await exists(targetPath(blueprint.name)))) {
|
|
272
|
+
throw new Error(`${plan.handler.kind} '${blueprint.name}' already exists; it appeared while planning. Run the command again to resolve the conflict.`);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const preparedFilesByPlan = new Map();
|
|
277
|
+
// Capture every selected versioned entry before any supporting template can
|
|
278
|
+
// mutate the shared context, and confirm the planned version still matches.
|
|
279
|
+
for (const plan of plans) {
|
|
280
|
+
for (const blueprint of plan.plan) {
|
|
281
|
+
if (blueprint.status !== "update" ||
|
|
282
|
+
!willInstallBlueprint(blueprint, plan.selectedNewBlueprints, plan.decisions) ||
|
|
283
|
+
blueprint.renderedEntry !== undefined ||
|
|
284
|
+
blueprint.versionedEntry === undefined) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const { sourcePath, outputPath } = blueprint.versionedEntry;
|
|
288
|
+
let renderedEntry;
|
|
289
|
+
const actual = await readBlueprintVersion(sourcePath, context, {
|
|
290
|
+
onRendered: (sourceText, renderedText) => {
|
|
291
|
+
renderedEntry = { sourcePath, outputPath, sourceText, renderedText };
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
if (actual !== blueprint.availableVersion || renderedEntry === undefined) {
|
|
295
|
+
throw new Error(`${plan.handler.kind} '${blueprint.name}' version changed while planning; run again.`);
|
|
296
|
+
}
|
|
297
|
+
blueprint.renderedEntry = renderedEntry;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
for (const plan of plans) {
|
|
301
|
+
const preparedFilesByName = new Map();
|
|
302
|
+
for (const blueprint of plan.plan) {
|
|
303
|
+
if (!willInstallBlueprint(blueprint, plan.selectedNewBlueprints, plan.decisions))
|
|
304
|
+
continue;
|
|
305
|
+
preparedFilesByName.set(blueprint.name, await prepareFiles(plan.sourceFilesByName.get(blueprint.name), context, plan.handler, blueprint.renderedEntry));
|
|
306
|
+
}
|
|
307
|
+
preparedFilesByPlan.set(plan, preparedFilesByName);
|
|
308
|
+
}
|
|
309
|
+
const configWrite = options.mode === "init"
|
|
310
|
+
? {
|
|
311
|
+
path: join(options.githubDirectory, "spindle.json"),
|
|
312
|
+
content: `${JSON.stringify(options.config, null, 2)}\n`,
|
|
313
|
+
}
|
|
314
|
+
: undefined;
|
|
315
|
+
const preparedInitializers = [];
|
|
316
|
+
try {
|
|
317
|
+
for (const { handler, selections, deletions } of initializers) {
|
|
318
|
+
preparedInitializers.push({
|
|
319
|
+
handler,
|
|
320
|
+
initialization: await handler.prepareInitialize(options.githubDirectory, context ?? { stacks, variables: {} }, selections, deletions),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
await Promise.all(preparedInitializers.map(({ initialization }) => initialization.dispose().catch(() => undefined)));
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
const completedChanges = [];
|
|
329
|
+
const recordAppliedChange = (description) => completedChanges.push(description);
|
|
330
|
+
try {
|
|
331
|
+
for (const plan of plans) {
|
|
332
|
+
outcomes.push(...(await applyReconciliation(plan, preparedFilesByPlan.get(plan), membershipCleanupOperations, installationService, recordAppliedChange)));
|
|
333
|
+
}
|
|
334
|
+
for (const { handler, initialization } of preparedInitializers) {
|
|
335
|
+
const initialized = await initialization.apply(recordAppliedChange);
|
|
336
|
+
for (const name of initialized.installed) {
|
|
337
|
+
outcomes.push({ kind: handler.kind, name, status: "installed" });
|
|
338
|
+
}
|
|
339
|
+
for (const name of initialized.removed) {
|
|
340
|
+
outcomes.push({ kind: handler.kind, name, status: "removed" });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
outcomes.push(...(await applyRemovalOperations(options.githubDirectory, deletionOperations
|
|
344
|
+
.filter((operation) => operation.handler.prepareInitialize === undefined)
|
|
345
|
+
.map(catalogRemovalOperation), recordAppliedChange)));
|
|
346
|
+
outcomes.push(...(await applyRemovalOperations(options.githubDirectory, membershipCleanupOperations.map(membershipRemovalOperation), recordAppliedChange)));
|
|
347
|
+
if (configWrite !== undefined) {
|
|
348
|
+
try {
|
|
349
|
+
await mkdir(options.githubDirectory, { recursive: true });
|
|
350
|
+
await writeFile(configWrite.path, configWrite.content);
|
|
351
|
+
recordAppliedChange("wrote Config '.github/spindle.json'");
|
|
352
|
+
}
|
|
353
|
+
catch (error) {
|
|
354
|
+
throw new Error(`Failed to write Config '${configWrite.path}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
360
|
+
const progress = completedChanges.length > 0 ? ` Completed changes: ${completedChanges.join(", ")}.` : "";
|
|
361
|
+
throw new Error(`${message}${progress} Application stopped; rollback was not attempted.`, {
|
|
362
|
+
cause: error,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
await Promise.all(preparedInitializers.map(({ initialization }) => initialization.dispose().catch(() => undefined)));
|
|
367
|
+
}
|
|
368
|
+
return outcomes;
|
|
369
|
+
}
|
|
370
|
+
export class BlueprintReconciliationService extends Service {
|
|
371
|
+
installationService;
|
|
372
|
+
constructor(app) {
|
|
373
|
+
super(app);
|
|
374
|
+
}
|
|
375
|
+
async boot() {
|
|
376
|
+
this.installationService = await this.app.make(InstallationService);
|
|
377
|
+
}
|
|
378
|
+
async reconcile(handlers, options) {
|
|
379
|
+
this.assertApplicationBooted();
|
|
380
|
+
const perform = () => performReconciliation(handlers, {
|
|
381
|
+
...options,
|
|
382
|
+
app: this.app,
|
|
383
|
+
githubDirectory: this.app.workspacePath(".github"),
|
|
384
|
+
blueprintSourceRoot: this.app.appPath("Assets/Blueprints"),
|
|
385
|
+
}, this.installationService);
|
|
386
|
+
if (options.mode === "init") {
|
|
387
|
+
return LoadProjectConfiguration.withTemporaryState(this.app, { status: "valid", data: options.config }, perform);
|
|
388
|
+
}
|
|
389
|
+
return perform();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { copyFile, mkdir, mkdtemp, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { assertSingleFileDestination } from "../Support/BlueprintFiles.js";
|
|
4
|
+
import { Service } from "../../Foundation/Service.js";
|
|
5
|
+
async function installFile(file, destinationPath, replace = false, requireText = false) {
|
|
6
|
+
await mkdir(dirname(destinationPath), { recursive: true });
|
|
7
|
+
if (replace && requireText) {
|
|
8
|
+
await assertSingleFileDestination(destinationPath, true);
|
|
9
|
+
const stagingDirectory = await mkdtemp(join(dirname(destinationPath), ".spindle-"));
|
|
10
|
+
try {
|
|
11
|
+
const stagedPath = join(stagingDirectory, basename(destinationPath));
|
|
12
|
+
await writeFile(stagedPath, file.renderedText, { flag: "wx" });
|
|
13
|
+
await assertSingleFileDestination(destinationPath, true);
|
|
14
|
+
await rename(stagedPath, destinationPath);
|
|
15
|
+
}
|
|
16
|
+
finally {
|
|
17
|
+
await rm(stagingDirectory, { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (replace)
|
|
22
|
+
await rm(destinationPath, { recursive: true, force: true });
|
|
23
|
+
if (file.renderedText === null) {
|
|
24
|
+
await copyFile(file.sourcePath, destinationPath);
|
|
25
|
+
}
|
|
26
|
+
else if (requireText) {
|
|
27
|
+
await writeFile(destinationPath, file.renderedText, { flag: "wx" });
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
await writeFile(destinationPath, file.renderedText);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export class InstallationService extends Service {
|
|
34
|
+
constructor(app) {
|
|
35
|
+
super(app);
|
|
36
|
+
}
|
|
37
|
+
async installPreparedFiles(files, target, entryFile, replace = false, atTarget = false, onFileApplied) {
|
|
38
|
+
this.assertApplicationBooted();
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
const destinationPath = atTarget
|
|
41
|
+
? target
|
|
42
|
+
: entryFile === undefined
|
|
43
|
+
? join(target, file.outputPath)
|
|
44
|
+
: target;
|
|
45
|
+
try {
|
|
46
|
+
await installFile(file, destinationPath, replace, entryFile !== undefined || (atTarget && file.outputPath.toLowerCase().endsWith(".md")));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw new Error(`Failed to apply file '${file.outputPath}' at '${destinationPath}': ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
50
|
+
}
|
|
51
|
+
onFileApplied?.(file, destinationPath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { access, lstat, readFile } from "node:fs/promises";
|
|
2
|
+
import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { renderText } from "../../TemplateRendering/Rendering/RenderText.js";
|
|
4
|
+
// Only known text formats are rendered; all other files are copied unchanged.
|
|
5
|
+
const textExtensions = new Set([
|
|
6
|
+
".md",
|
|
7
|
+
".txt",
|
|
8
|
+
".json",
|
|
9
|
+
".yml",
|
|
10
|
+
".yaml",
|
|
11
|
+
".toml",
|
|
12
|
+
".sh",
|
|
13
|
+
".ts",
|
|
14
|
+
".js",
|
|
15
|
+
".py",
|
|
16
|
+
".php",
|
|
17
|
+
".html",
|
|
18
|
+
".css",
|
|
19
|
+
".xml",
|
|
20
|
+
".j2",
|
|
21
|
+
".njk",
|
|
22
|
+
"",
|
|
23
|
+
]);
|
|
24
|
+
export async function exists(path) {
|
|
25
|
+
try {
|
|
26
|
+
await access(path);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export async function assertSafeInstallPaths(githubDirectory, blueprintNames, destinationDirectory = join(githubDirectory, "skills"), targetPath = (name) => join(destinationDirectory, name)) {
|
|
34
|
+
const installRoot = resolve(githubDirectory);
|
|
35
|
+
const paths = [
|
|
36
|
+
githubDirectory,
|
|
37
|
+
destinationDirectory,
|
|
38
|
+
join(githubDirectory, "spindle.json"),
|
|
39
|
+
...blueprintNames.map(targetPath),
|
|
40
|
+
];
|
|
41
|
+
for (const path of paths) {
|
|
42
|
+
const absolutePath = resolve(path);
|
|
43
|
+
const pathFromRoot = relative(installRoot, absolutePath);
|
|
44
|
+
if (pathFromRoot === ".." || pathFromRoot.startsWith(`..${sep}`) || isAbsolute(pathFromRoot)) {
|
|
45
|
+
throw new Error(`Refusing to write '${path}': target is outside .github.`);
|
|
46
|
+
}
|
|
47
|
+
let current = installRoot;
|
|
48
|
+
for (const segment of pathFromRoot === "" ? [] : pathFromRoot.split(sep)) {
|
|
49
|
+
current = join(current, segment);
|
|
50
|
+
try {
|
|
51
|
+
if ((await lstat(current)).isSymbolicLink()) {
|
|
52
|
+
throw new Error(`Refusing to write through symbolic link '${current}'. Spindle writes only inside .github.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error.code === "ENOENT")
|
|
57
|
+
break;
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
if ((await lstat(path)).isSymbolicLink()) {
|
|
63
|
+
throw new Error("Refusing to write through symbolic link '" +
|
|
64
|
+
path +
|
|
65
|
+
"'. Spindle writes only inside .github.");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (error.code !== "ENOENT")
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function assertSingleFileDestination(path, mustExist = false) {
|
|
75
|
+
let stat;
|
|
76
|
+
try {
|
|
77
|
+
stat = await lstat(path);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (!mustExist && error.code === "ENOENT")
|
|
81
|
+
return;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
if (stat.isSymbolicLink()) {
|
|
85
|
+
throw new Error(`Refusing to replace symbolic link '${path}'.`);
|
|
86
|
+
}
|
|
87
|
+
if (!stat.isFile()) {
|
|
88
|
+
throw new Error(`Refusing to overwrite '${path}': expected a regular file.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function preparePlannedBlueprintFiles(files, context, validateRenderedFile, requireText = false) {
|
|
92
|
+
const prepared = [];
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
const content = await readFile(file.sourcePath);
|
|
95
|
+
let text = null;
|
|
96
|
+
if (textExtensions.has(extname(file.sourcePath).toLowerCase()) && !content.includes(0)) {
|
|
97
|
+
try {
|
|
98
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(content);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Invalid UTF-8 is not renderable text; preserve the original bytes below.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (text === null && (typeof requireText === "function" ? requireText(file) : requireText)) {
|
|
105
|
+
throw new Error(`Blueprint entry '${file.outputPath}' must be valid UTF-8 text.`);
|
|
106
|
+
}
|
|
107
|
+
const renderedText = text === null ? null : renderText(text, context);
|
|
108
|
+
if (renderedText !== null)
|
|
109
|
+
validateRenderedFile?.(file.outputPath, renderedText);
|
|
110
|
+
prepared.push({ ...file, renderedText });
|
|
111
|
+
}
|
|
112
|
+
return prepared;
|
|
113
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import matter from "gray-matter";
|
|
3
|
+
import { renderText } from "../../TemplateRendering/Rendering/RenderText.js";
|
|
4
|
+
const semverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)?$/;
|
|
5
|
+
export function parseSemver(value) {
|
|
6
|
+
const match = semverPattern.exec(value);
|
|
7
|
+
if (!match)
|
|
8
|
+
return null;
|
|
9
|
+
return {
|
|
10
|
+
core: [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])],
|
|
11
|
+
prerelease: match[4] ? match[4].split(".") : [],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function comparePrerelease(a, b) {
|
|
15
|
+
// A version without a prerelease outranks one with it.
|
|
16
|
+
if (a.length === 0 || b.length === 0)
|
|
17
|
+
return b.length - a.length;
|
|
18
|
+
for (let i = 0; i < Math.min(a.length, b.length); i++) {
|
|
19
|
+
const numA = /^\d+$/.test(a[i]);
|
|
20
|
+
const numB = /^\d+$/.test(b[i]);
|
|
21
|
+
if (numA && numB) {
|
|
22
|
+
const valueA = BigInt(a[i]);
|
|
23
|
+
const valueB = BigInt(b[i]);
|
|
24
|
+
if (valueA !== valueB)
|
|
25
|
+
return valueA < valueB ? -1 : 1;
|
|
26
|
+
}
|
|
27
|
+
else if (numA !== numB) {
|
|
28
|
+
return numA ? -1 : 1;
|
|
29
|
+
}
|
|
30
|
+
else if (a[i] !== b[i]) {
|
|
31
|
+
return a[i] < b[i] ? -1 : 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return Math.sign(a.length - b.length);
|
|
35
|
+
}
|
|
36
|
+
export function compareSemver(a, b) {
|
|
37
|
+
for (let i = 0; i < 3; i++) {
|
|
38
|
+
if (a.core[i] !== b.core[i])
|
|
39
|
+
return a.core[i] < b.core[i] ? -1 : 1;
|
|
40
|
+
}
|
|
41
|
+
return comparePrerelease(a.prerelease, b.prerelease);
|
|
42
|
+
}
|
|
43
|
+
// Returns the raw Spindle-owned version value from entry-file frontmatter, or null when absent or unreadable.
|
|
44
|
+
export async function readBlueprintVersion(entryFile, context, options = {}) {
|
|
45
|
+
let content;
|
|
46
|
+
try {
|
|
47
|
+
content = await readFile(entryFile, "utf8");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const rendered = context
|
|
53
|
+
? renderText(content, {
|
|
54
|
+
stacks: [...context.stacks],
|
|
55
|
+
variables: { ...context.variables },
|
|
56
|
+
computed: { ...context.computed },
|
|
57
|
+
})
|
|
58
|
+
: content;
|
|
59
|
+
if (context)
|
|
60
|
+
options.onRendered?.(content, rendered);
|
|
61
|
+
try {
|
|
62
|
+
const version = matter(rendered, {}).data.metadata?.spindle?.version;
|
|
63
|
+
return typeof version === "string" || typeof version === "number" ? String(version) : null;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (options.rejectMalformed)
|
|
67
|
+
throw error;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|