@parlor/scaffold 0.0.0-stage → 0.2.0
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 +11 -2
- package/dist/add/addon.d.ts +103 -0
- package/dist/add/addon.d.ts.map +1 -0
- package/dist/add/addon.js +12 -0
- package/dist/add/addon.js.map +1 -0
- package/dist/add/plan.d.ts +72 -0
- package/dist/add/plan.d.ts.map +1 -0
- package/dist/add/plan.js +237 -0
- package/dist/add/plan.js.map +1 -0
- package/dist/add/run.d.ts +92 -0
- package/dist/add/run.d.ts.map +1 -0
- package/dist/add/run.js +167 -0
- package/dist/add/run.js.map +1 -0
- package/dist/add/transaction.d.ts +67 -0
- package/dist/add/transaction.d.ts.map +1 -0
- package/dist/add/transaction.js +181 -0
- package/dist/add/transaction.js.map +1 -0
- package/dist/addons/agent.d.ts +58 -0
- package/dist/addons/agent.d.ts.map +1 -0
- package/dist/addons/agent.js +50 -0
- package/dist/addons/agent.js.map +1 -0
- package/dist/addons/auth.d.ts +103 -0
- package/dist/addons/auth.d.ts.map +1 -0
- package/dist/addons/auth.js +93 -0
- package/dist/addons/auth.js.map +1 -0
- package/dist/addons/index.d.ts +159 -0
- package/dist/addons/index.d.ts.map +1 -0
- package/dist/addons/index.js +8 -0
- package/dist/addons/index.js.map +1 -0
- package/dist/catalog.d.ts +3 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +23 -0
- package/dist/catalog.js.map +1 -0
- package/dist/create.d.ts +45 -0
- package/dist/create.d.ts.map +1 -0
- package/dist/create.js +162 -0
- package/dist/create.js.map +1 -0
- package/dist/docs.d.ts +12 -0
- package/dist/docs.d.ts.map +1 -0
- package/dist/docs.js +70 -0
- package/dist/docs.js.map +1 -0
- package/dist/edit/inspect.d.ts +15 -0
- package/dist/edit/inspect.d.ts.map +1 -0
- package/dist/edit/inspect.js +73 -0
- package/dist/edit/inspect.js.map +1 -0
- package/dist/edit/ops.d.ts +92 -0
- package/dist/edit/ops.d.ts.map +1 -0
- package/dist/edit/ops.js +516 -0
- package/dist/edit/ops.js.map +1 -0
- package/dist/edit/source.d.ts +24 -0
- package/dist/edit/source.d.ts.map +1 -0
- package/dist/edit/source.js +36 -0
- package/dist/edit/source.js.map +1 -0
- package/dist/edit/syntax.d.ts +39 -0
- package/dist/edit/syntax.d.ts.map +1 -0
- package/dist/edit/syntax.js +129 -0
- package/dist/edit/syntax.js.map +1 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +5 -0
- package/dist/errors.js.map +1 -0
- package/dist/files.d.ts +15 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +65 -0
- package/dist/files.js.map +1 -0
- package/dist/fixtures.d.ts +28 -0
- package/dist/fixtures.d.ts.map +1 -0
- package/dist/fixtures.js +66 -0
- package/dist/fixtures.js.map +1 -0
- package/dist/git.d.ts +22 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +56 -0
- package/dist/git.js.map +1 -0
- package/dist/identity.d.ts +9 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +23 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/package-manager.d.ts +49 -0
- package/dist/package-manager.d.ts.map +1 -0
- package/dist/package-manager.js +118 -0
- package/dist/package-manager.js.map +1 -0
- package/dist/version.d.ts +6 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +9 -0
- package/dist/version.js.map +1 -0
- package/package.json +46 -3
- package/templates/auth/auth.ts +19 -0
- package/templates/auth/schema/auth.ts +1 -0
- package/templates/auth/src/auth/client.ts +3 -0
- package/templates/auth/src/auth/sign-in-gate.tsx +17 -0
- package/templates/auth/src/auth/sign-out-button.tsx +17 -0
- package/templates/auth/src/routes/link.tsx +92 -0
- package/templates/auth/src/routes/sign-in.tsx +117 -0
- package/templates/default/_gitignore +7 -0
- package/templates/default/actions/index.ts +58 -0
- package/templates/default/actions/todos.spec.ts +38 -0
- package/templates/default/index.html +13 -0
- package/templates/default/migrations/0000_init.sql +9 -0
- package/templates/default/migrations/meta/0000_snapshot.json +87 -0
- package/templates/default/migrations/meta/_journal.json +13 -0
- package/templates/default/package.json +44 -0
- package/templates/default/parlor.config.ts +12 -0
- package/templates/default/schema/index.ts +1 -0
- package/templates/default/schema/todos.ts +13 -0
- package/templates/default/server.ts +13 -0
- package/templates/default/src/agent/agent-button.tsx +120 -0
- package/templates/default/src/agent/agent-sidebar.tsx +828 -0
- package/templates/default/src/agent/agent.css +5 -0
- package/templates/default/src/agent/confirmation-card.tsx +42 -0
- package/templates/default/src/agent/sidebar-view.ts +36 -0
- package/templates/default/src/agent/views.ts +9 -0
- package/templates/default/src/app.css +1 -0
- package/templates/default/src/header-actions.tsx +9 -0
- package/templates/default/src/main.tsx +27 -0
- package/templates/default/src/parlor.d.ts +9 -0
- package/templates/default/src/routeTree.gen.ts +59 -0
- package/templates/default/src/routes/__root.tsx +29 -0
- package/templates/default/src/routes/index.tsx +141 -0
- package/templates/default/tsconfig.json +8 -0
- package/templates/default/vitest.config.ts +5 -0
package/dist/add/run.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { indentLines } from "../edit/ops.js";
|
|
2
|
+
import { projectFiles, templateFiles, writeFiles } from "../files.js";
|
|
3
|
+
import { uncommittedChanges } from "../git.js";
|
|
4
|
+
import { commandsFor, formatCommand } from "../package-manager.js";
|
|
5
|
+
import { planAddons } from "./plan.js";
|
|
6
|
+
import { commitStaging, discardStaging, generateInStaging, installDependencies, pendingInstallCommands, restoreManifests, snapshotManifests, stageProject, } from "./transaction.js";
|
|
7
|
+
/**
|
|
8
|
+
* The one entry point behind `parlor add`. A transaction in stages, so a failure never leaves
|
|
9
|
+
* an app that cannot boot and a crash is finished by the next run (every step it already did
|
|
10
|
+
* is `done`):
|
|
11
|
+
* 1. plan (no writes); `blocked`, `done` and `dry-run` return here
|
|
12
|
+
* 2. git: `uncommittedChanges(rootDir)` non-empty and `gitCheck` -> `dirty`
|
|
13
|
+
* 3. install: snapshot `package.json` and the lockfile; round one adds every dependency whose
|
|
14
|
+
* version is known (`<pm> add`), round two resolves each `FromPackage` from the manifest
|
|
15
|
+
* round one installed and adds those; any failure restores the snapshot -> `failed`
|
|
16
|
+
* 4. stage: `stageProject` copies the tree (minus node_modules, .git, .parlor) into
|
|
17
|
+
* `.parlor/add/<run id>/` and lays the planned writes over it
|
|
18
|
+
* 5. generate: each pending generation's tool runs in the staging dir; `generationDone`
|
|
19
|
+
* against the staging copy must then hold, else the copy is discarded -> `failed`
|
|
20
|
+
* 6. commit: every staged file whose bytes differ from the project is written through a temp
|
|
21
|
+
* file and a rename; the staging dir is removed
|
|
22
|
+
* `install: false` skips 3 and 5; `pending` then names the install commands and a second
|
|
23
|
+
* `parlor add` to generate.
|
|
24
|
+
*/
|
|
25
|
+
export async function addAddons(request) {
|
|
26
|
+
const { layout, addons, packageManager, log } = request;
|
|
27
|
+
const { rootDir } = layout;
|
|
28
|
+
const plan = await planAddons(layout, addons, projectFiles(rootDir), templateFiles());
|
|
29
|
+
if (plan.kind === "done")
|
|
30
|
+
return plan;
|
|
31
|
+
if (plan.kind === "blocked")
|
|
32
|
+
return { kind: "blocked", addons, manual: plan.manual };
|
|
33
|
+
if (request.mode === "dry-run")
|
|
34
|
+
return { kind: "planned", plan };
|
|
35
|
+
if (request.gitCheck) {
|
|
36
|
+
const files = await uncommittedChanges(rootDir);
|
|
37
|
+
if (files && files.length > 0)
|
|
38
|
+
return { kind: "dirty", addons, files };
|
|
39
|
+
}
|
|
40
|
+
if (!request.install) {
|
|
41
|
+
await writeFiles(rootDir, new Map(plan.writes.map((write) => [write.path, write.content])));
|
|
42
|
+
const pm = commandsFor(packageManager);
|
|
43
|
+
const rerun = plan.generate.length > 0 ||
|
|
44
|
+
plan.dependencies.some((dependency) => typeof dependency.version !== "string");
|
|
45
|
+
return {
|
|
46
|
+
kind: "applied",
|
|
47
|
+
plan,
|
|
48
|
+
generated: [],
|
|
49
|
+
pending: [
|
|
50
|
+
...pendingInstallCommands(plan.dependencies, packageManager).map(formatCommand),
|
|
51
|
+
...(rerun ? [pm.exec("parlor", ["add", ...addons])] : []),
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
let snapshot;
|
|
56
|
+
if (plan.dependencies.length > 0) {
|
|
57
|
+
snapshot = await snapshotManifests(rootDir);
|
|
58
|
+
const installed = await installDependencies(rootDir, plan.dependencies, packageManager, projectFiles(rootDir), log);
|
|
59
|
+
if (!installed.ok) {
|
|
60
|
+
await restoreManifests(rootDir, snapshot);
|
|
61
|
+
return { kind: "failed", addons, stage: "install", message: installed.message };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const staging = await stageProject(rootDir, plan.writes);
|
|
65
|
+
if (plan.generate.length > 0) {
|
|
66
|
+
log(`generating ${plan.generate
|
|
67
|
+
.map((generation) => generation.kind === "route-tree"
|
|
68
|
+
? "the route tree"
|
|
69
|
+
: `the migration "${generation.name}"`)
|
|
70
|
+
.join(", then ")}`);
|
|
71
|
+
const generated = await generateInStaging(layout, staging, plan.generate, request.tools);
|
|
72
|
+
if (!generated.ok) {
|
|
73
|
+
await discardStaging(staging);
|
|
74
|
+
if (snapshot)
|
|
75
|
+
await restoreManifests(rootDir, snapshot);
|
|
76
|
+
return { kind: "failed", addons, stage: "generate", message: generated.message };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const written = await commitStaging(rootDir, staging);
|
|
80
|
+
const planned = new Set(plan.writes.map((write) => write.path));
|
|
81
|
+
return {
|
|
82
|
+
kind: "applied",
|
|
83
|
+
plan,
|
|
84
|
+
generated: written.filter((path) => !planned.has(path)),
|
|
85
|
+
pending: [],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function planLines(plan, generated) {
|
|
89
|
+
const width = Math.max(0, ...plan.writes
|
|
90
|
+
.filter((write) => write.change === "update")
|
|
91
|
+
.map((write) => write.path.length));
|
|
92
|
+
return [
|
|
93
|
+
...plan.writes
|
|
94
|
+
.filter((write) => write.change === "create")
|
|
95
|
+
.map((write) => `+ ${write.path}`),
|
|
96
|
+
...plan.writes
|
|
97
|
+
.filter((write) => write.change === "update")
|
|
98
|
+
.map((write) => write.summary.length > 0
|
|
99
|
+
? `~ ${write.path.padEnd(width)} ${write.summary.join(", ")}`
|
|
100
|
+
: `~ ${write.path}`),
|
|
101
|
+
...(generated.length > 0 ? [`~ ${generated.join(", ")}`] : []),
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
/** The terminal text for a result and the exit code: 1 for `blocked`, `dirty` and `failed`, else 0. */
|
|
105
|
+
export function formatAddResult(result, manager) {
|
|
106
|
+
const pm = commandsFor(manager);
|
|
107
|
+
const command = (addons) => `parlor add ${addons.join(" ")}`;
|
|
108
|
+
switch (result.kind) {
|
|
109
|
+
case "done":
|
|
110
|
+
return {
|
|
111
|
+
text: `${result.addons.join(", ")} ${result.addons.length === 1 ? "is" : "are"} already added. Nothing changed.`,
|
|
112
|
+
exitCode: 0,
|
|
113
|
+
};
|
|
114
|
+
case "blocked": {
|
|
115
|
+
const files = [...new Set(result.manual.map((change) => change.file))];
|
|
116
|
+
const rerun = pm.exec("parlor", ["add", ...result.addons]);
|
|
117
|
+
return {
|
|
118
|
+
text: [
|
|
119
|
+
`${command(result.addons)}: cannot edit ${files.join(", ")} safely, so nothing was changed.`,
|
|
120
|
+
...result.manual.flatMap((change) => [
|
|
121
|
+
"",
|
|
122
|
+
` ${change.file}: ${change.reason}`,
|
|
123
|
+
` Make this change by hand, then run \`${rerun}\` again:`,
|
|
124
|
+
"",
|
|
125
|
+
indentLines(change.change, " "),
|
|
126
|
+
]),
|
|
127
|
+
].join("\n"),
|
|
128
|
+
exitCode: 1,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
case "dirty":
|
|
132
|
+
return {
|
|
133
|
+
text: `${command(result.addons)}: this project has uncommitted changes (${result.files.join(", ")}).\nCommit or stash them so the change is easy to review, or pass --no-git-check.`,
|
|
134
|
+
exitCode: 1,
|
|
135
|
+
};
|
|
136
|
+
case "failed":
|
|
137
|
+
return {
|
|
138
|
+
text: `${command(result.addons)}: ${result.message}\nNothing was changed.`,
|
|
139
|
+
exitCode: 1,
|
|
140
|
+
};
|
|
141
|
+
case "planned": {
|
|
142
|
+
const installs = result.plan.dependencies.map((dependency) => typeof dependency.version === "string"
|
|
143
|
+
? `${dependency.name}@${dependency.version}`
|
|
144
|
+
: `${dependency.name} (pinned by ${dependency.version.fromPackage})`);
|
|
145
|
+
return {
|
|
146
|
+
text: [
|
|
147
|
+
...planLines(result.plan, []),
|
|
148
|
+
...(installs.length > 0 ? [`would install ${installs.join(", ")}`] : []),
|
|
149
|
+
"Nothing was written (--dry-run).",
|
|
150
|
+
].join("\n"),
|
|
151
|
+
exitCode: 0,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
case "applied":
|
|
155
|
+
return {
|
|
156
|
+
text: [
|
|
157
|
+
...planLines(result.plan, result.generated),
|
|
158
|
+
...result.plan.notes,
|
|
159
|
+
...(result.pending.length > 0
|
|
160
|
+
? ["", "Run these to finish:", ...result.pending.map((line) => ` ${line}`)]
|
|
161
|
+
: []),
|
|
162
|
+
].join("\n"),
|
|
163
|
+
exitCode: 0,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/add/run.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AddonName } from \"../addons/index.js\";\nimport { indentLines } from \"../edit/ops.js\";\nimport { projectFiles, templateFiles, writeFiles } from \"../files.js\";\nimport { uncommittedChanges } from \"../git.js\";\nimport { commandsFor, formatCommand, type PackageManager } from \"../package-manager.js\";\nimport type { ProjectLayout } from \"./addon.js\";\nimport { type AddPlan, type ManualChange, planAddons } from \"./plan.js\";\nimport {\n commitStaging,\n discardStaging,\n generateInStaging,\n installDependencies,\n type ManifestSnapshot,\n pendingInstallCommands,\n restoreManifests,\n snapshotManifests,\n stageProject,\n} from \"./transaction.js\";\n\n/**\n * The framework tools a run needs after files are staged. `parlor add` passes the same code\n * `parlor typegen` and `parlor db generate` run, so this package stays free of the toolchain.\n * Each takes the directory to run in, because the transaction runs them against the staging\n * copy, never against the project.\n */\nexport interface AddTools {\n routeTree(rootDir: string): Promise<void>;\n migration(rootDir: string, name: string): Promise<void>;\n}\n\nexport interface AddRequest {\n readonly layout: ProjectLayout;\n readonly addons: readonly AddonName[];\n readonly packageManager: PackageManager;\n readonly tools: AddTools;\n /** `dry-run` plans and reports without writing or installing. */\n readonly mode: \"apply\" | \"dry-run\";\n /** `false` (`--no-install`) writes files only and reports the install and generate commands. */\n readonly install: boolean;\n /** `false` (`--no-git-check`) skips the refusal on uncommitted changes. */\n readonly gitCheck: boolean;\n readonly log: (line: string) => void;\n}\n\ntype ReadyPlan = Extract<AddPlan, { kind: \"ready\" }>;\n\nexport type AddResult =\n | { readonly kind: \"done\"; readonly addons: readonly AddonName[] }\n | {\n readonly kind: \"blocked\";\n readonly addons: readonly AddonName[];\n readonly manual: readonly ManualChange[];\n }\n | { readonly kind: \"planned\"; readonly plan: ReadyPlan }\n /** The project has uncommitted changes and `gitCheck` is on: nothing was touched. */\n | {\n readonly kind: \"dirty\";\n readonly addons: readonly AddonName[];\n readonly files: readonly string[];\n }\n /** `pending` lists commands skipped by `--no-install`, to run before the next `parlor dev`. */\n | {\n readonly kind: \"applied\";\n readonly plan: ReadyPlan;\n /** Files the generators wrote, beyond the plan's own writes. */\n readonly generated: readonly string[];\n readonly pending: readonly string[];\n }\n /**\n * Nothing reached the project: a failed install restored `package.json` and the lockfile; a\n * failed or unvalidated generation discarded the staging copy and restored them too. Only\n * `node_modules` may hold more than before, which is a cache, not state.\n */\n | {\n readonly kind: \"failed\";\n readonly addons: readonly AddonName[];\n readonly stage: \"install\" | \"generate\";\n readonly message: string;\n };\n\n/**\n * The one entry point behind `parlor add`. A transaction in stages, so a failure never leaves\n * an app that cannot boot and a crash is finished by the next run (every step it already did\n * is `done`):\n * 1. plan (no writes); `blocked`, `done` and `dry-run` return here\n * 2. git: `uncommittedChanges(rootDir)` non-empty and `gitCheck` -> `dirty`\n * 3. install: snapshot `package.json` and the lockfile; round one adds every dependency whose\n * version is known (`<pm> add`), round two resolves each `FromPackage` from the manifest\n * round one installed and adds those; any failure restores the snapshot -> `failed`\n * 4. stage: `stageProject` copies the tree (minus node_modules, .git, .parlor) into\n * `.parlor/add/<run id>/` and lays the planned writes over it\n * 5. generate: each pending generation's tool runs in the staging dir; `generationDone`\n * against the staging copy must then hold, else the copy is discarded -> `failed`\n * 6. commit: every staged file whose bytes differ from the project is written through a temp\n * file and a rename; the staging dir is removed\n * `install: false` skips 3 and 5; `pending` then names the install commands and a second\n * `parlor add` to generate.\n */\nexport async function addAddons(request: AddRequest): Promise<AddResult> {\n const { layout, addons, packageManager, log } = request;\n const { rootDir } = layout;\n const plan = await planAddons(layout, addons, projectFiles(rootDir), templateFiles());\n if (plan.kind === \"done\") return plan;\n if (plan.kind === \"blocked\") return { kind: \"blocked\", addons, manual: plan.manual };\n if (request.mode === \"dry-run\") return { kind: \"planned\", plan };\n if (request.gitCheck) {\n const files = await uncommittedChanges(rootDir);\n if (files && files.length > 0) return { kind: \"dirty\", addons, files };\n }\n if (!request.install) {\n await writeFiles(\n rootDir,\n new Map(plan.writes.map((write) => [write.path, write.content])),\n );\n const pm = commandsFor(packageManager);\n const rerun =\n plan.generate.length > 0 ||\n plan.dependencies.some((dependency) => typeof dependency.version !== \"string\");\n return {\n kind: \"applied\",\n plan,\n generated: [],\n pending: [\n ...pendingInstallCommands(plan.dependencies, packageManager).map(formatCommand),\n ...(rerun ? [pm.exec(\"parlor\", [\"add\", ...addons])] : []),\n ],\n };\n }\n let snapshot: ManifestSnapshot | undefined;\n if (plan.dependencies.length > 0) {\n snapshot = await snapshotManifests(rootDir);\n const installed = await installDependencies(\n rootDir,\n plan.dependencies,\n packageManager,\n projectFiles(rootDir),\n log,\n );\n if (!installed.ok) {\n await restoreManifests(rootDir, snapshot);\n return { kind: \"failed\", addons, stage: \"install\", message: installed.message };\n }\n }\n const staging = await stageProject(rootDir, plan.writes);\n if (plan.generate.length > 0) {\n log(\n `generating ${plan.generate\n .map((generation) =>\n generation.kind === \"route-tree\"\n ? \"the route tree\"\n : `the migration \"${generation.name}\"`,\n )\n .join(\", then \")}`,\n );\n const generated = await generateInStaging(\n layout,\n staging,\n plan.generate,\n request.tools,\n );\n if (!generated.ok) {\n await discardStaging(staging);\n if (snapshot) await restoreManifests(rootDir, snapshot);\n return { kind: \"failed\", addons, stage: \"generate\", message: generated.message };\n }\n }\n const written = await commitStaging(rootDir, staging);\n const planned = new Set(plan.writes.map((write) => write.path));\n return {\n kind: \"applied\",\n plan,\n generated: written.filter((path) => !planned.has(path)),\n pending: [],\n };\n}\n\nfunction planLines(plan: ReadyPlan, generated: readonly string[]): string[] {\n const width = Math.max(\n 0,\n ...plan.writes\n .filter((write) => write.change === \"update\")\n .map((write) => write.path.length),\n );\n return [\n ...plan.writes\n .filter((write) => write.change === \"create\")\n .map((write) => `+ ${write.path}`),\n ...plan.writes\n .filter((write) => write.change === \"update\")\n .map((write) =>\n write.summary.length > 0\n ? `~ ${write.path.padEnd(width)} ${write.summary.join(\", \")}`\n : `~ ${write.path}`,\n ),\n ...(generated.length > 0 ? [`~ ${generated.join(\", \")}`] : []),\n ];\n}\n\n/** The terminal text for a result and the exit code: 1 for `blocked`, `dirty` and `failed`, else 0. */\nexport function formatAddResult(\n result: AddResult,\n manager: PackageManager,\n): { readonly text: string; readonly exitCode: 0 | 1 } {\n const pm = commandsFor(manager);\n const command = (addons: readonly AddonName[]) => `parlor add ${addons.join(\" \")}`;\n switch (result.kind) {\n case \"done\":\n return {\n text: `${result.addons.join(\", \")} ${result.addons.length === 1 ? \"is\" : \"are\"} already added. Nothing changed.`,\n exitCode: 0,\n };\n case \"blocked\": {\n const files = [...new Set(result.manual.map((change) => change.file))];\n const rerun = pm.exec(\"parlor\", [\"add\", ...result.addons]);\n return {\n text: [\n `${command(result.addons)}: cannot edit ${files.join(\", \")} safely, so nothing was changed.`,\n ...result.manual.flatMap((change) => [\n \"\",\n ` ${change.file}: ${change.reason}`,\n ` Make this change by hand, then run \\`${rerun}\\` again:`,\n \"\",\n indentLines(change.change, \" \"),\n ]),\n ].join(\"\\n\"),\n exitCode: 1,\n };\n }\n case \"dirty\":\n return {\n text: `${command(result.addons)}: this project has uncommitted changes (${result.files.join(\", \")}).\\nCommit or stash them so the change is easy to review, or pass --no-git-check.`,\n exitCode: 1,\n };\n case \"failed\":\n return {\n text: `${command(result.addons)}: ${result.message}\\nNothing was changed.`,\n exitCode: 1,\n };\n case \"planned\": {\n const installs = result.plan.dependencies.map((dependency) =>\n typeof dependency.version === \"string\"\n ? `${dependency.name}@${dependency.version}`\n : `${dependency.name} (pinned by ${dependency.version.fromPackage})`,\n );\n return {\n text: [\n ...planLines(result.plan, []),\n ...(installs.length > 0 ? [`would install ${installs.join(\", \")}`] : []),\n \"Nothing was written (--dry-run).\",\n ].join(\"\\n\"),\n exitCode: 0,\n };\n }\n case \"applied\":\n return {\n text: [\n ...planLines(result.plan, result.generated),\n ...result.plan.notes,\n ...(result.pending.length > 0\n ? [\"\", \"Run these to finish:\", ...result.pending.map((line) => ` ${line}`)]\n : []),\n ].join(\"\\n\"),\n exitCode: 0,\n };\n }\n}\n"]}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type Command, type InstallOutcome, type PackageManager } from "../package-manager.js";
|
|
2
|
+
import type { Generation, ProjectLayout } from "./addon.js";
|
|
3
|
+
import { type PlannedDependency, type PlannedWrite, type ProjectFiles } from "./plan.js";
|
|
4
|
+
import type { AddTools } from "./run.js";
|
|
5
|
+
/** Under `.parlor/`, which every project ignores, so a crash leaves nothing a commit would pick up. */
|
|
6
|
+
export declare const STAGING_DIR = ".parlor/add";
|
|
7
|
+
/** Never copied into a staging dir and never committed back from one. */
|
|
8
|
+
export declare const NOT_STAGED: ReadonlySet<string>;
|
|
9
|
+
/** `package.json` plus whichever lockfile the project has, as they were before the install. */
|
|
10
|
+
export interface ManifestSnapshot {
|
|
11
|
+
readonly files: ReadonlyMap<string, Buffer | undefined>;
|
|
12
|
+
}
|
|
13
|
+
export declare const LOCKFILES: readonly ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lock", "bun.lockb"];
|
|
14
|
+
export declare function snapshotManifests(rootDir: string): Promise<ManifestSnapshot>;
|
|
15
|
+
/** Puts every snapshotted file back, deleting the ones that did not exist. */
|
|
16
|
+
export declare function restoreManifests(rootDir: string, snapshot: ManifestSnapshot): Promise<void>;
|
|
17
|
+
export type InstallResult = {
|
|
18
|
+
readonly ok: true;
|
|
19
|
+
readonly outcome: InstallOutcome;
|
|
20
|
+
} | {
|
|
21
|
+
readonly ok: false;
|
|
22
|
+
readonly message: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Two rounds of `<pm> add`, because a `FromPackage` version is readable only once that package
|
|
26
|
+
* is in `node_modules`: round one adds every dependency with a known version, round two reads
|
|
27
|
+
* each `FromPackage` through `project.manifest` and adds those. A `listed` dependency with no
|
|
28
|
+
* known version is a plain `<pm> install`. Nothing here touches files other than the ones the
|
|
29
|
+
* package manager owns.
|
|
30
|
+
*/
|
|
31
|
+
export declare function installDependencies(rootDir: string, dependencies: readonly PlannedDependency[], manager: PackageManager, project: ProjectFiles, log: (line: string) => void): Promise<InstallResult>;
|
|
32
|
+
/** The install commands `--no-install` left for the developer, in the order to run them. */
|
|
33
|
+
export declare function pendingInstallCommands(dependencies: readonly PlannedDependency[], manager: PackageManager): readonly Command[];
|
|
34
|
+
export interface Staging {
|
|
35
|
+
/** Absolute: `<rootDir>/.parlor/add/<run id>`. */
|
|
36
|
+
readonly dir: string;
|
|
37
|
+
/** A `ProjectFiles` over `dir`, for `generationDone` and the commit diff. */
|
|
38
|
+
readonly files: ProjectFiles;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Copies the project tree except `NOT_STAGED` into a fresh staging dir and lays `writes` over
|
|
42
|
+
* it. Module resolution from the staging dir walks up to the project's `node_modules`, so the
|
|
43
|
+
* generators run there unchanged.
|
|
44
|
+
*/
|
|
45
|
+
export declare function stageProject(rootDir: string, writes: readonly PlannedWrite[]): Promise<Staging>;
|
|
46
|
+
export type GenerateResult = {
|
|
47
|
+
readonly ok: true;
|
|
48
|
+
} | {
|
|
49
|
+
readonly ok: false;
|
|
50
|
+
readonly generation: Generation;
|
|
51
|
+
readonly message: string;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Runs each generation's tool in the staging dir, then checks `generationDone` there. A tool
|
|
55
|
+
* that throws, or whose declared outputs are not in the copy afterwards, fails the whole run
|
|
56
|
+
* before anything reaches the project.
|
|
57
|
+
*/
|
|
58
|
+
export declare function generateInStaging(layout: ProjectLayout, staging: Staging, generations: readonly Generation[], tools: AddTools): Promise<GenerateResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Every file in the staging dir whose bytes differ from the project's is written through
|
|
61
|
+
* `<path>.parlor-tmp` and a rename, then the staging dir is removed. Returns the project paths
|
|
62
|
+
* written. A crash between two renames leaves whole files, and the next run's plan finds each
|
|
63
|
+
* written step `done`.
|
|
64
|
+
*/
|
|
65
|
+
export declare function commitStaging(rootDir: string, staging: Staging): Promise<readonly string[]>;
|
|
66
|
+
export declare function discardStaging(staging: Staging): Promise<void>;
|
|
67
|
+
//# sourceMappingURL=transaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../../src/add/transaction.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { cp, mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { projectFiles } from "../files.js";
|
|
5
|
+
import { commandsFor, formatCommand, runInstall, } from "../package-manager.js";
|
|
6
|
+
import { generationDone, resolveFromPackage, } from "./plan.js";
|
|
7
|
+
/** Under `.parlor/`, which every project ignores, so a crash leaves nothing a commit would pick up. */
|
|
8
|
+
export const STAGING_DIR = ".parlor/add";
|
|
9
|
+
/** Never copied into a staging dir and never committed back from one. */
|
|
10
|
+
export const NOT_STAGED = new Set([
|
|
11
|
+
"node_modules",
|
|
12
|
+
".git",
|
|
13
|
+
".parlor",
|
|
14
|
+
]);
|
|
15
|
+
export const LOCKFILES = [
|
|
16
|
+
"pnpm-lock.yaml",
|
|
17
|
+
"package-lock.json",
|
|
18
|
+
"yarn.lock",
|
|
19
|
+
"bun.lock",
|
|
20
|
+
"bun.lockb",
|
|
21
|
+
];
|
|
22
|
+
export async function snapshotManifests(rootDir) {
|
|
23
|
+
const files = new Map();
|
|
24
|
+
for (const name of ["package.json", ...LOCKFILES]) {
|
|
25
|
+
files.set(name, await readFile(join(rootDir, name)).catch(() => undefined));
|
|
26
|
+
}
|
|
27
|
+
return { files };
|
|
28
|
+
}
|
|
29
|
+
/** Puts every snapshotted file back, deleting the ones that did not exist. */
|
|
30
|
+
export async function restoreManifests(rootDir, snapshot) {
|
|
31
|
+
for (const [name, content] of snapshot.files) {
|
|
32
|
+
const path = join(rootDir, name);
|
|
33
|
+
if (content === undefined)
|
|
34
|
+
await rm(path, { force: true });
|
|
35
|
+
else
|
|
36
|
+
await writeFile(path, content);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Two rounds of `<pm> add`, because a `FromPackage` version is readable only once that package
|
|
41
|
+
* is in `node_modules`: round one adds every dependency with a known version, round two reads
|
|
42
|
+
* each `FromPackage` through `project.manifest` and adds those. A `listed` dependency with no
|
|
43
|
+
* known version is a plain `<pm> install`. Nothing here touches files other than the ones the
|
|
44
|
+
* package manager owns.
|
|
45
|
+
*/
|
|
46
|
+
export async function installDependencies(rootDir, dependencies, manager, project, log) {
|
|
47
|
+
const add = commandsFor(manager).add;
|
|
48
|
+
let outcome = { kind: "installed" };
|
|
49
|
+
const round = async (resolved) => {
|
|
50
|
+
for (const dev of [false, true]) {
|
|
51
|
+
const group = resolved.filter((dependency) => dependency.dev === dev);
|
|
52
|
+
if (group.length === 0)
|
|
53
|
+
continue;
|
|
54
|
+
const command = add(group.map((dependency) => `${dependency.name}@${dependency.version}`), { dev });
|
|
55
|
+
const result = await runInstall(rootDir, command, log);
|
|
56
|
+
if (result.kind === "failed") {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
message: `${formatCommand(command)} failed (exit ${result.exitCode}).`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (result.kind === "ignored-builds")
|
|
63
|
+
outcome = result;
|
|
64
|
+
}
|
|
65
|
+
return undefined;
|
|
66
|
+
};
|
|
67
|
+
const known = dependencies.filter((dependency) => typeof dependency.version === "string");
|
|
68
|
+
if (known.length > 0) {
|
|
69
|
+
log(`installing ${known.map((dependency) => `${dependency.name}@${dependency.version}`).join(", ")}`);
|
|
70
|
+
const failed = await round(known);
|
|
71
|
+
if (failed)
|
|
72
|
+
return failed;
|
|
73
|
+
}
|
|
74
|
+
const pinned = [];
|
|
75
|
+
for (const dependency of dependencies) {
|
|
76
|
+
if (typeof dependency.version === "string")
|
|
77
|
+
continue;
|
|
78
|
+
const version = await resolveFromPackage(dependency.version, project);
|
|
79
|
+
if (version === undefined) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
message: `${dependency.version.fromPackage} does not declare ${dependency.version.dependency}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
log(`installing ${dependency.name}@${version} (pinned by ${dependency.version.fromPackage})`);
|
|
86
|
+
pinned.push({ ...dependency, version });
|
|
87
|
+
}
|
|
88
|
+
const failed = await round(pinned);
|
|
89
|
+
return failed ?? { ok: true, outcome };
|
|
90
|
+
}
|
|
91
|
+
/** The install commands `--no-install` left for the developer, in the order to run them. */
|
|
92
|
+
export function pendingInstallCommands(dependencies, manager) {
|
|
93
|
+
const add = commandsFor(manager).add;
|
|
94
|
+
return [false, true].flatMap((dev) => {
|
|
95
|
+
const group = dependencies.filter((dependency) => dependency.dev === dev && typeof dependency.version === "string");
|
|
96
|
+
return group.length === 0
|
|
97
|
+
? []
|
|
98
|
+
: [
|
|
99
|
+
add(group.map((dependency) => `${dependency.name}@${String(dependency.version)}`), { dev }),
|
|
100
|
+
];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Copies the project tree except `NOT_STAGED` into a fresh staging dir and lays `writes` over
|
|
105
|
+
* it. Module resolution from the staging dir walks up to the project's `node_modules`, so the
|
|
106
|
+
* generators run there unchanged.
|
|
107
|
+
*/
|
|
108
|
+
export async function stageProject(rootDir, writes) {
|
|
109
|
+
const dir = join(rootDir, STAGING_DIR, randomUUID());
|
|
110
|
+
await mkdir(dir, { recursive: true });
|
|
111
|
+
for (const entry of await readdir(rootDir)) {
|
|
112
|
+
if (NOT_STAGED.has(entry))
|
|
113
|
+
continue;
|
|
114
|
+
await cp(join(rootDir, entry), join(dir, entry), { recursive: true });
|
|
115
|
+
}
|
|
116
|
+
for (const write of writes) {
|
|
117
|
+
const target = join(dir, write.path);
|
|
118
|
+
await mkdir(dirname(target), { recursive: true });
|
|
119
|
+
await writeFile(target, write.content);
|
|
120
|
+
}
|
|
121
|
+
return { dir, files: projectFiles(dir) };
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Runs each generation's tool in the staging dir, then checks `generationDone` there. A tool
|
|
125
|
+
* that throws, or whose declared outputs are not in the copy afterwards, fails the whole run
|
|
126
|
+
* before anything reaches the project.
|
|
127
|
+
*/
|
|
128
|
+
export async function generateInStaging(layout, staging, generations, tools) {
|
|
129
|
+
for (const generation of generations) {
|
|
130
|
+
const tool = generation.kind === "route-tree" ? "parlor typegen" : "parlor db generate";
|
|
131
|
+
try {
|
|
132
|
+
if (generation.kind === "route-tree")
|
|
133
|
+
await tools.routeTree(staging.dir);
|
|
134
|
+
else
|
|
135
|
+
await tools.migration(staging.dir, generation.name);
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
generation,
|
|
141
|
+
message: `${tool} failed: ${error.message}`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
if (!(await generationDone({ ...layout, rootDir: staging.dir }, generation, staging.files))) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
generation,
|
|
148
|
+
message: generation.kind === "route-tree"
|
|
149
|
+
? `${tool} wrote no route ${generation.routes.join(", ")}`
|
|
150
|
+
: `${tool} wrote no migration with tables ${generation.tables.join(", ")}`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return { ok: true };
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Every file in the staging dir whose bytes differ from the project's is written through
|
|
158
|
+
* `<path>.parlor-tmp` and a rename, then the staging dir is removed. Returns the project paths
|
|
159
|
+
* written. A crash between two renames leaves whole files, and the next run's plan finds each
|
|
160
|
+
* written step `done`.
|
|
161
|
+
*/
|
|
162
|
+
export async function commitStaging(rootDir, staging) {
|
|
163
|
+
const written = [];
|
|
164
|
+
for (const path of await staging.files.list("")) {
|
|
165
|
+
const staged = await readFile(join(staging.dir, path));
|
|
166
|
+
const current = await readFile(join(rootDir, path)).catch(() => undefined);
|
|
167
|
+
if (current?.equals(staged))
|
|
168
|
+
continue;
|
|
169
|
+
const target = join(rootDir, path);
|
|
170
|
+
await mkdir(dirname(target), { recursive: true });
|
|
171
|
+
await writeFile(`${target}.parlor-tmp`, staged);
|
|
172
|
+
await rename(`${target}.parlor-tmp`, target);
|
|
173
|
+
written.push(path);
|
|
174
|
+
}
|
|
175
|
+
await discardStaging(staging);
|
|
176
|
+
return written;
|
|
177
|
+
}
|
|
178
|
+
export async function discardStaging(staging) {
|
|
179
|
+
await rm(staging.dir, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
//# sourceMappingURL=transaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction.js","sourceRoot":"","sources":["../../src/add/transaction.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport { cp, mkdir, readdir, readFile, rename, rm, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\nimport { projectFiles } from \"../files.js\";\nimport {\n type Command,\n commandsFor,\n formatCommand,\n type InstallOutcome,\n type PackageManager,\n runInstall,\n} from \"../package-manager.js\";\nimport type { Generation, ProjectLayout } from \"./addon.js\";\nimport {\n generationDone,\n type PlannedDependency,\n type PlannedWrite,\n type ProjectFiles,\n resolveFromPackage,\n} from \"./plan.js\";\nimport type { AddTools } from \"./run.js\";\n\n/** Under `.parlor/`, which every project ignores, so a crash leaves nothing a commit would pick up. */\nexport const STAGING_DIR = \".parlor/add\";\n\n/** Never copied into a staging dir and never committed back from one. */\nexport const NOT_STAGED: ReadonlySet<string> = new Set([\n \"node_modules\",\n \".git\",\n \".parlor\",\n]);\n\n/** `package.json` plus whichever lockfile the project has, as they were before the install. */\nexport interface ManifestSnapshot {\n readonly files: ReadonlyMap<string, Buffer | undefined>;\n}\n\nexport const LOCKFILES = [\n \"pnpm-lock.yaml\",\n \"package-lock.json\",\n \"yarn.lock\",\n \"bun.lock\",\n \"bun.lockb\",\n] as const;\n\nexport async function snapshotManifests(rootDir: string): Promise<ManifestSnapshot> {\n const files = new Map<string, Buffer | undefined>();\n for (const name of [\"package.json\", ...LOCKFILES]) {\n files.set(name, await readFile(join(rootDir, name)).catch(() => undefined));\n }\n return { files };\n}\n\n/** Puts every snapshotted file back, deleting the ones that did not exist. */\nexport async function restoreManifests(\n rootDir: string,\n snapshot: ManifestSnapshot,\n): Promise<void> {\n for (const [name, content] of snapshot.files) {\n const path = join(rootDir, name);\n if (content === undefined) await rm(path, { force: true });\n else await writeFile(path, content);\n }\n}\n\nexport type InstallResult =\n | { readonly ok: true; readonly outcome: InstallOutcome }\n | { readonly ok: false; readonly message: string };\n\n/**\n * Two rounds of `<pm> add`, because a `FromPackage` version is readable only once that package\n * is in `node_modules`: round one adds every dependency with a known version, round two reads\n * each `FromPackage` through `project.manifest` and adds those. A `listed` dependency with no\n * known version is a plain `<pm> install`. Nothing here touches files other than the ones the\n * package manager owns.\n */\nexport async function installDependencies(\n rootDir: string,\n dependencies: readonly PlannedDependency[],\n manager: PackageManager,\n project: ProjectFiles,\n log: (line: string) => void,\n): Promise<InstallResult> {\n const add = commandsFor(manager).add;\n let outcome: InstallOutcome = { kind: \"installed\" };\n const round = async (\n resolved: readonly (PlannedDependency & { readonly version: string })[],\n ): Promise<InstallResult | undefined> => {\n for (const dev of [false, true]) {\n const group = resolved.filter((dependency) => dependency.dev === dev);\n if (group.length === 0) continue;\n const command = add(\n group.map((dependency) => `${dependency.name}@${dependency.version}`),\n { dev },\n );\n const result = await runInstall(rootDir, command, log);\n if (result.kind === \"failed\") {\n return {\n ok: false,\n message: `${formatCommand(command)} failed (exit ${result.exitCode}).`,\n };\n }\n if (result.kind === \"ignored-builds\") outcome = result;\n }\n return undefined;\n };\n const known = dependencies.filter(\n (dependency): dependency is PlannedDependency & { readonly version: string } =>\n typeof dependency.version === \"string\",\n );\n if (known.length > 0) {\n log(\n `installing ${known.map((dependency) => `${dependency.name}@${dependency.version}`).join(\", \")}`,\n );\n const failed = await round(known);\n if (failed) return failed;\n }\n const pinned: (PlannedDependency & { readonly version: string })[] = [];\n for (const dependency of dependencies) {\n if (typeof dependency.version === \"string\") continue;\n const version = await resolveFromPackage(dependency.version, project);\n if (version === undefined) {\n return {\n ok: false,\n message: `${dependency.version.fromPackage} does not declare ${dependency.version.dependency}`,\n };\n }\n log(\n `installing ${dependency.name}@${version} (pinned by ${dependency.version.fromPackage})`,\n );\n pinned.push({ ...dependency, version });\n }\n const failed = await round(pinned);\n return failed ?? { ok: true, outcome };\n}\n\n/** The install commands `--no-install` left for the developer, in the order to run them. */\nexport function pendingInstallCommands(\n dependencies: readonly PlannedDependency[],\n manager: PackageManager,\n): readonly Command[] {\n const add = commandsFor(manager).add;\n return [false, true].flatMap((dev) => {\n const group = dependencies.filter(\n (dependency) => dependency.dev === dev && typeof dependency.version === \"string\",\n );\n return group.length === 0\n ? []\n : [\n add(\n group.map((dependency) => `${dependency.name}@${String(dependency.version)}`),\n { dev },\n ),\n ];\n });\n}\n\nexport interface Staging {\n /** Absolute: `<rootDir>/.parlor/add/<run id>`. */\n readonly dir: string;\n /** A `ProjectFiles` over `dir`, for `generationDone` and the commit diff. */\n readonly files: ProjectFiles;\n}\n\n/**\n * Copies the project tree except `NOT_STAGED` into a fresh staging dir and lays `writes` over\n * it. Module resolution from the staging dir walks up to the project's `node_modules`, so the\n * generators run there unchanged.\n */\nexport async function stageProject(\n rootDir: string,\n writes: readonly PlannedWrite[],\n): Promise<Staging> {\n const dir = join(rootDir, STAGING_DIR, randomUUID());\n await mkdir(dir, { recursive: true });\n for (const entry of await readdir(rootDir)) {\n if (NOT_STAGED.has(entry)) continue;\n await cp(join(rootDir, entry), join(dir, entry), { recursive: true });\n }\n for (const write of writes) {\n const target = join(dir, write.path);\n await mkdir(dirname(target), { recursive: true });\n await writeFile(target, write.content);\n }\n return { dir, files: projectFiles(dir) };\n}\n\nexport type GenerateResult =\n | { readonly ok: true }\n | { readonly ok: false; readonly generation: Generation; readonly message: string };\n\n/**\n * Runs each generation's tool in the staging dir, then checks `generationDone` there. A tool\n * that throws, or whose declared outputs are not in the copy afterwards, fails the whole run\n * before anything reaches the project.\n */\nexport async function generateInStaging(\n layout: ProjectLayout,\n staging: Staging,\n generations: readonly Generation[],\n tools: AddTools,\n): Promise<GenerateResult> {\n for (const generation of generations) {\n const tool =\n generation.kind === \"route-tree\" ? \"parlor typegen\" : \"parlor db generate\";\n try {\n if (generation.kind === \"route-tree\") await tools.routeTree(staging.dir);\n else await tools.migration(staging.dir, generation.name);\n } catch (error) {\n return {\n ok: false,\n generation,\n message: `${tool} failed: ${(error as Error).message}`,\n };\n }\n if (\n !(await generationDone(\n { ...layout, rootDir: staging.dir },\n generation,\n staging.files,\n ))\n ) {\n return {\n ok: false,\n generation,\n message:\n generation.kind === \"route-tree\"\n ? `${tool} wrote no route ${generation.routes.join(\", \")}`\n : `${tool} wrote no migration with tables ${generation.tables.join(\", \")}`,\n };\n }\n }\n return { ok: true };\n}\n\n/**\n * Every file in the staging dir whose bytes differ from the project's is written through\n * `<path>.parlor-tmp` and a rename, then the staging dir is removed. Returns the project paths\n * written. A crash between two renames leaves whole files, and the next run's plan finds each\n * written step `done`.\n */\nexport async function commitStaging(\n rootDir: string,\n staging: Staging,\n): Promise<readonly string[]> {\n const written: string[] = [];\n for (const path of await staging.files.list(\"\")) {\n const staged = await readFile(join(staging.dir, path));\n const current = await readFile(join(rootDir, path)).catch(() => undefined);\n if (current?.equals(staged)) continue;\n const target = join(rootDir, path);\n await mkdir(dirname(target), { recursive: true });\n await writeFile(`${target}.parlor-tmp`, staged);\n await rename(`${target}.parlor-tmp`, target);\n written.push(path);\n }\n await discardStaging(staging);\n return written;\n}\n\nexport async function discardStaging(staging: Staging): Promise<void> {\n await rm(staging.dir, { recursive: true, force: true });\n}\n"]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { SERVER_ENTRY } from "../add/addon.js";
|
|
2
|
+
/**
|
|
3
|
+
* The default template already contains all of this; a test runs this add-on on it and expects
|
|
4
|
+
* no change, which is what keeps the two from drifting.
|
|
5
|
+
*/
|
|
6
|
+
export declare const agent: {
|
|
7
|
+
readonly summary: "The agent sidebar: chat with your own coding agent inside the app.";
|
|
8
|
+
readonly requires: {
|
|
9
|
+
readonly routesDir: "src/routes";
|
|
10
|
+
};
|
|
11
|
+
readonly dependencies: {
|
|
12
|
+
readonly "@parlor/chat": "framework";
|
|
13
|
+
};
|
|
14
|
+
readonly files: readonly [{
|
|
15
|
+
readonly template: "default";
|
|
16
|
+
readonly dir: "src/agent";
|
|
17
|
+
}];
|
|
18
|
+
readonly edits: readonly [{
|
|
19
|
+
readonly file: typeof SERVER_ENTRY;
|
|
20
|
+
readonly ops: readonly [{
|
|
21
|
+
readonly kind: "import";
|
|
22
|
+
readonly names: readonly ["chat"];
|
|
23
|
+
readonly from: "@parlor/chat/server";
|
|
24
|
+
}, {
|
|
25
|
+
readonly kind: "append-element";
|
|
26
|
+
readonly call: {
|
|
27
|
+
readonly callee: "createServer";
|
|
28
|
+
readonly from: "@parlor/server";
|
|
29
|
+
};
|
|
30
|
+
readonly path: readonly ["plugins"];
|
|
31
|
+
readonly element: "chat()";
|
|
32
|
+
}];
|
|
33
|
+
}, {
|
|
34
|
+
readonly file: "src/routes/__root.tsx";
|
|
35
|
+
readonly ops: readonly [{
|
|
36
|
+
readonly kind: "import";
|
|
37
|
+
readonly names: readonly ["agentPanel"];
|
|
38
|
+
readonly from: "../agent/views.js";
|
|
39
|
+
}, {
|
|
40
|
+
readonly kind: "append-element";
|
|
41
|
+
readonly call: import("../edit/syntax.js").CallSite;
|
|
42
|
+
readonly path: readonly ["staticData", "views"];
|
|
43
|
+
readonly element: "agentPanel";
|
|
44
|
+
}];
|
|
45
|
+
}];
|
|
46
|
+
readonly mounts: readonly [{
|
|
47
|
+
readonly component: "AgentButton";
|
|
48
|
+
readonly module: "src/agent/agent-button.tsx";
|
|
49
|
+
readonly into: {
|
|
50
|
+
readonly file: "src/header-actions.tsx";
|
|
51
|
+
readonly parent: "HeaderActions";
|
|
52
|
+
};
|
|
53
|
+
}];
|
|
54
|
+
readonly env: readonly [];
|
|
55
|
+
readonly generate: readonly [];
|
|
56
|
+
readonly notes: readonly ["Restart the dev server. Open the Agent button to chat with the coding agents on this computer."];
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.d.ts","sourceRoot":"","sources":["../../src/addons/agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAExE;;;GAGG;AACH,eAAO,MAAM,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2ChB,CAAC"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { defineAddon, ROOT_ROUTE, SERVER_ENTRY } from "../add/addon.js";
|
|
2
|
+
/**
|
|
3
|
+
* The default template already contains all of this; a test runs this add-on on it and expects
|
|
4
|
+
* no change, which is what keeps the two from drifting.
|
|
5
|
+
*/
|
|
6
|
+
export const agent = defineAddon({
|
|
7
|
+
summary: "The agent sidebar: chat with your own coding agent inside the app.",
|
|
8
|
+
requires: { routesDir: "src/routes" },
|
|
9
|
+
dependencies: { "@parlor/chat": "framework" },
|
|
10
|
+
files: [{ template: "default", dir: "src/agent" }],
|
|
11
|
+
edits: [
|
|
12
|
+
{
|
|
13
|
+
file: SERVER_ENTRY,
|
|
14
|
+
ops: [
|
|
15
|
+
{ kind: "import", names: ["chat"], from: "@parlor/chat/server" },
|
|
16
|
+
{
|
|
17
|
+
kind: "append-element",
|
|
18
|
+
call: { callee: "createServer", from: "@parlor/server" },
|
|
19
|
+
path: ["plugins"],
|
|
20
|
+
element: "chat()",
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
file: "src/routes/__root.tsx",
|
|
26
|
+
ops: [
|
|
27
|
+
{ kind: "import", names: ["agentPanel"], from: "../agent/views.js" },
|
|
28
|
+
{
|
|
29
|
+
kind: "append-element",
|
|
30
|
+
call: ROOT_ROUTE,
|
|
31
|
+
path: ["staticData", "views"],
|
|
32
|
+
element: "agentPanel",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
mounts: [
|
|
38
|
+
{
|
|
39
|
+
component: "AgentButton",
|
|
40
|
+
module: "src/agent/agent-button.tsx",
|
|
41
|
+
into: { file: "src/header-actions.tsx", parent: "HeaderActions" },
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
env: [],
|
|
45
|
+
generate: [],
|
|
46
|
+
notes: [
|
|
47
|
+
"Restart the dev server. Open the Agent button to chat with the coding agents on this computer.",
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
//# sourceMappingURL=agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent.js","sourceRoot":"","sources":["../../src/addons/agent.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAExE;;;GAGG;AACH,MAAM,CAAC,MAAM,KAAK,GAAG,WAAW,CAAC;IAC/B,OAAO,EAAE,oEAAoE;IAC7E,QAAQ,EAAE,EAAE,SAAS,EAAE,YAAY,EAAE;IACrC,YAAY,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE;IAC7C,KAAK,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,EAAE,WAAW,EAAE,CAAC;IAClD,KAAK,EAAE;QACL;YACE,IAAI,EAAE,YAAY;YAClB,GAAG,EAAE;gBACH,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,qBAAqB,EAAE;gBAChE;oBACE,IAAI,EAAE,gBAAgB;oBACtB,IAAI,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,gBAAgB,EAAE;oBACxD,IAAI,EAAE,CAAC,SAAS,CAAC;oBACjB,OAAO,EAAE,QAAQ;iBAClB;aACF;SACF;QACD;YACE,IAAI,EAAE,uBAAuB;YAC7B,GAAG,EAAE;gBACH,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE;gBACpE;oBACE,IAAI,EAAE,gBAAgB;oBACtB,IAAI,EAAE,UAAU;oBAChB,IAAI,EAAE,CAAC,YAAY,EAAE,OAAO,CAAC;oBAC7B,OAAO,EAAE,YAAY;iBACtB;aACF;SACF;KACF;IACD,MAAM,EAAE;QACN;YACE,SAAS,EAAE,aAAa;YACxB,MAAM,EAAE,4BAA4B;YACpC,IAAI,EAAE,EAAE,IAAI,EAAE,wBAAwB,EAAE,MAAM,EAAE,eAAe,EAAE;SAClE;KACF;IACD,GAAG,EAAE,EAAE;IACP,QAAQ,EAAE,EAAE;IACZ,KAAK,EAAE;QACL,gGAAgG;KACjG;CACF,CAAC,CAAC","sourcesContent":["import { defineAddon, ROOT_ROUTE, SERVER_ENTRY } from \"../add/addon.js\";\n\n/**\n * The default template already contains all of this; a test runs this add-on on it and expects\n * no change, which is what keeps the two from drifting.\n */\nexport const agent = defineAddon({\n summary: \"The agent sidebar: chat with your own coding agent inside the app.\",\n requires: { routesDir: \"src/routes\" },\n dependencies: { \"@parlor/chat\": \"framework\" },\n files: [{ template: \"default\", dir: \"src/agent\" }],\n edits: [\n {\n file: SERVER_ENTRY,\n ops: [\n { kind: \"import\", names: [\"chat\"], from: \"@parlor/chat/server\" },\n {\n kind: \"append-element\",\n call: { callee: \"createServer\", from: \"@parlor/server\" },\n path: [\"plugins\"],\n element: \"chat()\",\n },\n ],\n },\n {\n file: \"src/routes/__root.tsx\",\n ops: [\n { kind: \"import\", names: [\"agentPanel\"], from: \"../agent/views.js\" },\n {\n kind: \"append-element\",\n call: ROOT_ROUTE,\n path: [\"staticData\", \"views\"],\n element: \"agentPanel\",\n },\n ],\n },\n ],\n mounts: [\n {\n component: \"AgentButton\",\n module: \"src/agent/agent-button.tsx\",\n into: { file: \"src/header-actions.tsx\", parent: \"HeaderActions\" },\n },\n ],\n env: [],\n generate: [],\n notes: [\n \"Restart the dev server. Open the Agent button to chat with the coding agents on this computer.\",\n ],\n});\n"]}
|