@treeseed/sdk 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/operations/providers/default.js +1 -0
- package/dist/operations/services/config-runtime.d.ts +121 -26
- package/dist/operations/services/config-runtime.js +330 -196
- package/dist/operations/services/export-runtime.d.ts +18 -0
- package/dist/operations/services/export-runtime.js +136 -0
- package/dist/operations/services/template-registry.d.ts +2 -0
- package/dist/operations/services/template-registry.js +55 -6
- package/dist/operations-registry.js +1 -0
- package/dist/operations-types.d.ts +1 -1
- package/dist/platform/book-export.d.ts +78 -0
- package/dist/platform/book-export.js +449 -0
- package/dist/platform/contracts.d.ts +5 -0
- package/dist/platform/deploy-config.d.ts +2 -0
- package/dist/platform/deploy-config.js +30 -2
- package/dist/platform/env.yaml +5 -0
- package/dist/platform/environment.d.ts +10 -1
- package/dist/platform/environment.js +82 -6
- package/dist/scripts/aggregate-book.js +13 -118
- package/dist/scripts/config-treeseed.js +18 -27
- package/dist/sdk-types.d.ts +1 -0
- package/dist/template-catalog.js +1 -0
- package/dist/workflow/operations.d.ts +293 -177
- package/dist/workflow/operations.js +302 -188
- package/dist/workflow/policy.d.ts +12 -0
- package/dist/workflow/policy.js +58 -0
- package/dist/workflow-state.d.ts +19 -1
- package/dist/workflow-state.js +57 -39
- package/dist/workflow-support.d.ts +2 -1
- package/dist/workflow-support.js +14 -6
- package/dist/workflow.d.ts +14 -1
- package/dist/workflow.js +8 -1
- package/package.json +6 -1
|
@@ -1,23 +1,21 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
4
|
import {
|
|
5
5
|
applyTreeseedEnvironmentToProcess,
|
|
6
|
+
applyTreeseedConfigValues,
|
|
7
|
+
applyTreeseedSafeRepairs,
|
|
6
8
|
assertTreeseedCommandEnvironment,
|
|
7
9
|
checkTreeseedProviderConnections,
|
|
8
|
-
|
|
10
|
+
collectTreeseedConfigContext,
|
|
11
|
+
collectTreeseedPrintEnvReport,
|
|
9
12
|
ensureTreeseedActVerificationTooling,
|
|
10
13
|
ensureTreeseedGitignoreEntries,
|
|
11
|
-
|
|
12
|
-
formatTreeseedProviderConnectionReport,
|
|
14
|
+
finalizeTreeseedConfig,
|
|
13
15
|
getTreeseedMachineConfigPaths,
|
|
14
|
-
|
|
15
|
-
resolveTreeseedMachineEnvironmentValues,
|
|
16
|
-
rotateTreeseedMachineKey,
|
|
17
|
-
runTreeseedConfigWizard,
|
|
18
|
-
writeTreeseedLocalEnvironmentFiles,
|
|
19
|
-
writeTreeseedMachineConfig
|
|
16
|
+
rotateTreeseedMachineKey
|
|
20
17
|
} from "../operations/services/config-runtime.js";
|
|
18
|
+
import { exportTreeseedCodebase } from "../operations/services/export-runtime.js";
|
|
21
19
|
import {
|
|
22
20
|
assertDeploymentInitialized,
|
|
23
21
|
cleanupDestroyedState,
|
|
@@ -73,6 +71,10 @@ import {
|
|
|
73
71
|
} from "../operations/services/workspace-save.js";
|
|
74
72
|
import { run, workspaceRoot } from "../operations/services/workspace-tools.js";
|
|
75
73
|
import { resolveTreeseedWorkflowState } from "../workflow-state.js";
|
|
74
|
+
import {
|
|
75
|
+
classifyTreeseedBranchRole,
|
|
76
|
+
resolveTreeseedWorkflowPaths
|
|
77
|
+
} from "./policy.js";
|
|
76
78
|
class TreeseedWorkflowError extends Error {
|
|
77
79
|
code;
|
|
78
80
|
operation;
|
|
@@ -125,17 +127,6 @@ function withContextEnv(env, action) {
|
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
}
|
|
128
|
-
function runChild(command, args, context, cwd, label) {
|
|
129
|
-
const result = spawnSync(command, args, {
|
|
130
|
-
cwd,
|
|
131
|
-
env: { ...process.env, ...context.env ?? {} },
|
|
132
|
-
stdio: "inherit"
|
|
133
|
-
});
|
|
134
|
-
if (result.status !== 0) {
|
|
135
|
-
workflowError("dev", "unsupported_state", `${label} failed.`, { exitCode: result.status ?? 1 });
|
|
136
|
-
}
|
|
137
|
-
return result;
|
|
138
|
-
}
|
|
139
130
|
function runNodeScript(scriptName, context, cwd, label) {
|
|
140
131
|
const result = spawnSync(process.execPath, [packageScriptPath(scriptName)], {
|
|
141
132
|
cwd,
|
|
@@ -157,50 +148,51 @@ function normalizeConfigScopes(input) {
|
|
|
157
148
|
}
|
|
158
149
|
return ["local", "staging", "prod"].filter((scope) => requested.includes(scope));
|
|
159
150
|
}
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
return actions.filter((action) => {
|
|
163
|
-
if (seen.has(action.id)) return false;
|
|
164
|
-
seen.add(action.id);
|
|
165
|
-
return true;
|
|
166
|
-
});
|
|
151
|
+
function resolveWorkflowStateSnapshot(cwd) {
|
|
152
|
+
return resolveTreeseedWorkflowState(cwd);
|
|
167
153
|
}
|
|
168
|
-
function
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const envLocalPath = resolve(tenantRoot, ".env.local");
|
|
173
|
-
const envLocalExamplePath = resolve(tenantRoot, ".env.local.example");
|
|
174
|
-
if (!existsSync(envLocalPath) && existsSync(envLocalExamplePath)) {
|
|
175
|
-
copyFileSync(envLocalExamplePath, envLocalPath);
|
|
176
|
-
actions.push({ id: "env-local", detail: "Created .env.local from .env.local.example." });
|
|
177
|
-
}
|
|
178
|
-
const deployConfig = loadCliDeployConfig(tenantRoot);
|
|
179
|
-
const { configPath } = getTreeseedMachineConfigPaths(tenantRoot);
|
|
180
|
-
if (!existsSync(configPath)) {
|
|
181
|
-
const machineConfig2 = createDefaultTreeseedMachineConfig({
|
|
182
|
-
tenantRoot,
|
|
183
|
-
deployConfig,
|
|
184
|
-
tenantConfig: void 0
|
|
185
|
-
});
|
|
186
|
-
writeTreeseedMachineConfig(tenantRoot, machineConfig2);
|
|
187
|
-
actions.push({ id: "machine-config", detail: "Created the default Treeseed machine config." });
|
|
154
|
+
function resolveProjectRootOrThrow(operation, cwd) {
|
|
155
|
+
const resolved = resolveTreeseedWorkflowPaths(cwd);
|
|
156
|
+
if (!resolved.tenantRoot) {
|
|
157
|
+
workflowError(operation, "validation_failed", `Treeseed ${operation} requires a Treeseed project. Run the command from inside a tenant or initialize one first.`);
|
|
188
158
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
159
|
+
return resolved.cwd;
|
|
160
|
+
}
|
|
161
|
+
function resolveRepoState(repoDir) {
|
|
162
|
+
const branchName = currentBranch(repoDir) || null;
|
|
163
|
+
return {
|
|
164
|
+
repoDir,
|
|
165
|
+
branchName,
|
|
166
|
+
branchRole: classifyTreeseedBranchRole(branchName, repoDir),
|
|
167
|
+
dirtyWorktree: gitStatusPorcelain(repoDir).length > 0
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function buildWorkflowResult(operation, cwd, payload, nextSteps) {
|
|
171
|
+
return {
|
|
172
|
+
ok: true,
|
|
173
|
+
operation,
|
|
174
|
+
payload: {
|
|
175
|
+
...payload,
|
|
176
|
+
finalState: resolveWorkflowStateSnapshot(cwd)
|
|
177
|
+
},
|
|
178
|
+
nextSteps
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function ensureLocalReadinessOrThrow(operation, tenantRoot) {
|
|
182
|
+
const state = resolveWorkflowStateSnapshot(tenantRoot);
|
|
183
|
+
if (!state.readiness.local.ready) {
|
|
184
|
+
workflowError(
|
|
185
|
+
operation,
|
|
186
|
+
"validation_failed",
|
|
187
|
+
[
|
|
188
|
+
`Treeseed ${operation} requires the local environment to be configured.`,
|
|
189
|
+
...state.readiness.local.blockers,
|
|
190
|
+
"Run `treeseed config --environment local` first."
|
|
191
|
+
].join("\n"),
|
|
192
|
+
{ details: { readiness: state.readiness.local } }
|
|
193
|
+
);
|
|
202
194
|
}
|
|
203
|
-
return
|
|
195
|
+
return state;
|
|
204
196
|
}
|
|
205
197
|
function bumpRootPackageJson(root, level) {
|
|
206
198
|
const packageJsonPath = resolve(root, "package.json");
|
|
@@ -342,6 +334,56 @@ function resolveDestroyConfirmation(context, expected, input) {
|
|
|
342
334
|
}
|
|
343
335
|
return false;
|
|
344
336
|
}
|
|
337
|
+
function syncCurrentBranchToOrigin(operation, repoDir, branch) {
|
|
338
|
+
try {
|
|
339
|
+
if (remoteBranchExists(repoDir, branch)) {
|
|
340
|
+
run("git", ["pull", "--rebase", "origin", branch], { cwd: repoDir });
|
|
341
|
+
run("git", ["push", "origin", branch], { cwd: repoDir });
|
|
342
|
+
return {
|
|
343
|
+
remoteBranchExisted: true,
|
|
344
|
+
pulledRebase: true,
|
|
345
|
+
pushed: true,
|
|
346
|
+
createdRemoteBranch: false,
|
|
347
|
+
conflicts: false
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
run("git", ["push", "-u", "origin", branch], { cwd: repoDir });
|
|
351
|
+
return {
|
|
352
|
+
remoteBranchExisted: false,
|
|
353
|
+
pulledRebase: false,
|
|
354
|
+
pushed: true,
|
|
355
|
+
createdRemoteBranch: true,
|
|
356
|
+
conflicts: false
|
|
357
|
+
};
|
|
358
|
+
} catch {
|
|
359
|
+
const report = collectMergeConflictReport(repoDir);
|
|
360
|
+
throw new TreeseedWorkflowError(operation, "merge_conflict", formatMergeConflictReport(report, repoDir, branch), {
|
|
361
|
+
details: { branch, report },
|
|
362
|
+
exitCode: 12
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function maybeAutoSaveCurrentTaskBranch(helpers, operation, input) {
|
|
367
|
+
const tenantRoot = resolveProjectRootOrThrow(operation, helpers.cwd());
|
|
368
|
+
const repoDir = gitWorkflowRoot(tenantRoot);
|
|
369
|
+
const before = resolveRepoState(repoDir);
|
|
370
|
+
if (!before.dirtyWorktree) {
|
|
371
|
+
return { performed: false, save: null };
|
|
372
|
+
}
|
|
373
|
+
if (input.autoSave === false) {
|
|
374
|
+
workflowError(operation, "validation_failed", `Treeseed ${operation} requires a clean worktree or autoSave enabled.`);
|
|
375
|
+
}
|
|
376
|
+
const saveResult = await workflowSave(helpers, {
|
|
377
|
+
message: operation === "close" ? `close: ${input.message}` : input.message,
|
|
378
|
+
verify: input.verify === true,
|
|
379
|
+
refreshPreview: false,
|
|
380
|
+
preview: input.preview
|
|
381
|
+
});
|
|
382
|
+
return {
|
|
383
|
+
performed: true,
|
|
384
|
+
save: saveResult.payload
|
|
385
|
+
};
|
|
386
|
+
}
|
|
345
387
|
async function workflowStatus(helpers) {
|
|
346
388
|
return withContextEnv(helpers.context.env, () => createStatusResult(helpers.cwd()));
|
|
347
389
|
}
|
|
@@ -351,7 +393,7 @@ async function workflowTasks(helpers) {
|
|
|
351
393
|
async function workflowConfig(helpers, input = {}) {
|
|
352
394
|
try {
|
|
353
395
|
return await withContextEnv(helpers.context.env, async () => {
|
|
354
|
-
const tenantRoot = helpers.cwd();
|
|
396
|
+
const tenantRoot = resolveProjectRootOrThrow("config", helpers.cwd());
|
|
355
397
|
const scopes = normalizeConfigScopes(input);
|
|
356
398
|
const sync = input.syncProviders ?? input.sync ?? "all";
|
|
357
399
|
const printEnv = input.printEnv === true;
|
|
@@ -367,23 +409,22 @@ async function workflowConfig(helpers, input = {}) {
|
|
|
367
409
|
});
|
|
368
410
|
ensureTreeseedGitignoreEntries(tenantRoot);
|
|
369
411
|
const preflight = collectCliPreflight({ cwd: tenantRoot, requireAuth: false });
|
|
412
|
+
const contextSnapshot = collectTreeseedConfigContext({
|
|
413
|
+
tenantRoot,
|
|
414
|
+
scopes,
|
|
415
|
+
env: helpers.context.env
|
|
416
|
+
});
|
|
370
417
|
if (printEnvOnly) {
|
|
371
|
-
const
|
|
418
|
+
const reports2 = scopes.map((scope) => ({
|
|
372
419
|
scope,
|
|
373
|
-
|
|
420
|
+
environment: collectTreeseedPrintEnvReport({
|
|
374
421
|
tenantRoot,
|
|
375
422
|
scope,
|
|
376
423
|
env: helpers.context.env,
|
|
377
424
|
revealSecrets
|
|
378
425
|
}),
|
|
379
|
-
|
|
380
|
-
checkTreeseedProviderConnections({ tenantRoot, scope, env: helpers.context.env })
|
|
381
|
-
)
|
|
426
|
+
provider: checkTreeseedProviderConnections({ tenantRoot, scope, env: helpers.context.env })
|
|
382
427
|
}));
|
|
383
|
-
for (const report of reports) {
|
|
384
|
-
maybePrint(helpers.write, report.environmentReport);
|
|
385
|
-
maybePrint(helpers.write, report.providerReport);
|
|
386
|
-
}
|
|
387
428
|
return {
|
|
388
429
|
ok: true,
|
|
389
430
|
operation: "config",
|
|
@@ -392,7 +433,7 @@ async function workflowConfig(helpers, input = {}) {
|
|
|
392
433
|
scopes,
|
|
393
434
|
sync,
|
|
394
435
|
secretsRevealed: revealSecrets,
|
|
395
|
-
reports,
|
|
436
|
+
reports: reports2,
|
|
396
437
|
repairs,
|
|
397
438
|
preflight,
|
|
398
439
|
toolHealth
|
|
@@ -421,31 +462,46 @@ async function workflowConfig(helpers, input = {}) {
|
|
|
421
462
|
])
|
|
422
463
|
};
|
|
423
464
|
}
|
|
424
|
-
const
|
|
465
|
+
const explicitUpdates = Array.isArray(input.updates) ? input.updates.map((update) => ({
|
|
466
|
+
scope: update.scope,
|
|
467
|
+
entryId: String(update.entryId ?? ""),
|
|
468
|
+
value: typeof update.value === "string" ? update.value : "",
|
|
469
|
+
reused: update.reused === true
|
|
470
|
+
})) : null;
|
|
471
|
+
const autoUpdates = scopes.flatMap(
|
|
472
|
+
(scope) => contextSnapshot.entriesByScope[scope].map((entry) => ({
|
|
473
|
+
scope,
|
|
474
|
+
entryId: entry.id,
|
|
475
|
+
value: entry.effectiveValue,
|
|
476
|
+
reused: entry.currentValue.length > 0 || entry.suggestedValue.length > 0
|
|
477
|
+
}))
|
|
478
|
+
);
|
|
479
|
+
const applyResult = applyTreeseedConfigValues({
|
|
480
|
+
tenantRoot,
|
|
481
|
+
updates: explicitUpdates ?? autoUpdates
|
|
482
|
+
});
|
|
483
|
+
const finalizeResult = finalizeTreeseedConfig({
|
|
425
484
|
tenantRoot,
|
|
426
485
|
scopes,
|
|
427
486
|
sync,
|
|
428
|
-
|
|
429
|
-
env: helpers.context.env,
|
|
430
|
-
useInk: input.nonInteractive === true ? false : process.stdin.isTTY && process.stdout.isTTY,
|
|
431
|
-
printEnv,
|
|
432
|
-
revealSecrets,
|
|
433
|
-
write: (line) => maybePrint(helpers.write, line),
|
|
434
|
-
prompt: async (message) => {
|
|
435
|
-
if (input.nonInteractive === true) {
|
|
436
|
-
return "";
|
|
437
|
-
}
|
|
438
|
-
return String(await (helpers.context.prompt?.(message) ?? ""));
|
|
439
|
-
}
|
|
487
|
+
env: helpers.context.env
|
|
440
488
|
});
|
|
441
|
-
|
|
442
|
-
|
|
489
|
+
const reports = printEnv ? scopes.map((scope) => ({
|
|
490
|
+
scope,
|
|
491
|
+
environment: collectTreeseedPrintEnvReport({
|
|
492
|
+
tenantRoot,
|
|
493
|
+
scope,
|
|
494
|
+
env: helpers.context.env,
|
|
495
|
+
revealSecrets
|
|
496
|
+
}),
|
|
497
|
+
provider: finalizeResult.connectionChecks.find((report) => report.scope === scope) ?? checkTreeseedProviderConnections({ tenantRoot, scope, env: helpers.context.env })
|
|
498
|
+
})) : [];
|
|
443
499
|
const { configPath, keyPath } = getTreeseedMachineConfigPaths(tenantRoot);
|
|
444
500
|
const state = resolveTreeseedWorkflowState(tenantRoot);
|
|
445
|
-
return
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
501
|
+
return buildWorkflowResult(
|
|
502
|
+
"config",
|
|
503
|
+
tenantRoot,
|
|
504
|
+
{
|
|
449
505
|
mode: "configure",
|
|
450
506
|
scopes,
|
|
451
507
|
sync,
|
|
@@ -454,24 +510,37 @@ async function workflowConfig(helpers, input = {}) {
|
|
|
454
510
|
repairs,
|
|
455
511
|
preflight,
|
|
456
512
|
toolHealth,
|
|
457
|
-
|
|
458
|
-
|
|
513
|
+
context: contextSnapshot,
|
|
514
|
+
result: {
|
|
515
|
+
...applyResult,
|
|
516
|
+
...finalizeResult
|
|
517
|
+
},
|
|
518
|
+
reports,
|
|
519
|
+
state,
|
|
520
|
+
readiness: state.readiness
|
|
459
521
|
},
|
|
460
|
-
|
|
522
|
+
createNextSteps([
|
|
461
523
|
...scopes.includes("local") ? [{ operation: "dev", reason: "Start the local Treeseed runtime on the initialized local environment." }] : [],
|
|
462
524
|
...scopes.includes("staging") ? [{ operation: "status", reason: "Confirm staging readiness after initializing shared services." }] : [],
|
|
463
525
|
{ operation: "switch", reason: "Create or resume a task branch once the runtime foundation is ready.", input: { branch: "feature/my-change", preview: true } }
|
|
464
526
|
])
|
|
465
|
-
|
|
527
|
+
);
|
|
466
528
|
});
|
|
467
529
|
} catch (error) {
|
|
468
530
|
toError("config", error);
|
|
469
531
|
}
|
|
470
532
|
}
|
|
533
|
+
async function workflowExport(helpers, input = {}) {
|
|
534
|
+
return await withContextEnv(helpers.context.env, async () => {
|
|
535
|
+
const directory = resolve(helpers.context.cwd ?? helpers.cwd(), input.directory ?? ".");
|
|
536
|
+
const exported = await exportTreeseedCodebase({ directory });
|
|
537
|
+
return buildWorkflowResult("export", exported.tenantRoot, exported);
|
|
538
|
+
});
|
|
539
|
+
}
|
|
471
540
|
async function workflowSwitch(helpers, input) {
|
|
472
541
|
try {
|
|
473
542
|
return withContextEnv(helpers.context.env, () => {
|
|
474
|
-
const tenantRoot = helpers.cwd();
|
|
543
|
+
const tenantRoot = resolveProjectRootOrThrow("switch", helpers.cwd());
|
|
475
544
|
const branchName = String(input.branch ?? input.branchName ?? "").trim();
|
|
476
545
|
if (!branchName) {
|
|
477
546
|
workflowError("switch", "validation_failed", "Treeseed switch requires a branch name.");
|
|
@@ -503,10 +572,10 @@ async function workflowSwitch(helpers, input) {
|
|
|
503
572
|
previewResult = deployBranchPreview(tenantRoot, branchName, helpers.context, { initialize: true });
|
|
504
573
|
}
|
|
505
574
|
const state = resolveTreeseedWorkflowState(tenantRoot);
|
|
506
|
-
return
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
575
|
+
return buildWorkflowResult(
|
|
576
|
+
"switch",
|
|
577
|
+
tenantRoot,
|
|
578
|
+
{
|
|
510
579
|
branchName,
|
|
511
580
|
created,
|
|
512
581
|
resumed,
|
|
@@ -517,13 +586,16 @@ async function workflowSwitch(helpers, input) {
|
|
|
517
586
|
lastDeploymentTimestamp: state.preview.lastDeploymentTimestamp
|
|
518
587
|
},
|
|
519
588
|
previewResult,
|
|
520
|
-
|
|
589
|
+
preconditions: {
|
|
590
|
+
cleanWorktreeRequired: true,
|
|
591
|
+
baseBranch: STAGING_BRANCH
|
|
592
|
+
}
|
|
521
593
|
},
|
|
522
|
-
|
|
523
|
-
state.preview.enabled ? { operation: "save", reason: "Persist and verify the current task branch, then refresh its preview deployment.", input: { message: "describe your change" } } : { operation: "dev", reason: "Start the local development environment for this task branch." },
|
|
594
|
+
createNextSteps([
|
|
595
|
+
state.preview.enabled ? { operation: "save", reason: "Persist and verify the current task branch, then refresh its preview deployment.", input: { message: "describe your change", preview: true } } : { operation: "dev", reason: "Start the local development environment for this task branch." },
|
|
524
596
|
{ operation: "stage", reason: "Merge the task into staging once the task branch is verified.", input: { message: "describe the resolution" } }
|
|
525
597
|
])
|
|
526
|
-
|
|
598
|
+
);
|
|
527
599
|
});
|
|
528
600
|
} catch (error) {
|
|
529
601
|
toError("switch", error);
|
|
@@ -535,7 +607,8 @@ async function workflowDev(helpers, input = {}) {
|
|
|
535
607
|
if (helpers.context.transport === "api") {
|
|
536
608
|
workflowError("dev", "unsupported_transport", "Treeseed dev is not supported over the HTTP workflow API.");
|
|
537
609
|
}
|
|
538
|
-
const tenantRoot = helpers.cwd();
|
|
610
|
+
const tenantRoot = resolveProjectRootOrThrow("dev", helpers.cwd());
|
|
611
|
+
const readiness = ensureLocalReadinessOrThrow("dev", tenantRoot);
|
|
539
612
|
applyTreeseedEnvironmentToProcess({ tenantRoot, scope: "local", override: true });
|
|
540
613
|
assertTreeseedCommandEnvironment({ tenantRoot, scope: "local", purpose: "dev" });
|
|
541
614
|
const args = [packageScriptPath("tenant-dev")];
|
|
@@ -553,38 +626,42 @@ async function workflowDev(helpers, input = {}) {
|
|
|
553
626
|
stdio: input.stdio ?? "inherit",
|
|
554
627
|
detached: process.platform !== "win32"
|
|
555
628
|
});
|
|
556
|
-
return {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
629
|
+
return buildWorkflowResult("dev", tenantRoot, {
|
|
630
|
+
watch: input.watch === true,
|
|
631
|
+
background: true,
|
|
632
|
+
command: process.execPath,
|
|
633
|
+
args,
|
|
634
|
+
cwd: tenantRoot,
|
|
635
|
+
pid: child.pid ?? null,
|
|
636
|
+
exitCode: null,
|
|
637
|
+
runtime: {
|
|
638
|
+
mode: process.env.TREESEED_LOCAL_DEV_MODE ?? "cloudflare",
|
|
639
|
+
apiBaseUrl: process.env.TREESEED_API_BASE_URL ?? "http://127.0.0.1:3000",
|
|
640
|
+
webUrl: "http://127.0.0.1:8787"
|
|
641
|
+
},
|
|
642
|
+
readiness: readiness.readiness.local
|
|
643
|
+
});
|
|
569
644
|
}
|
|
570
645
|
const result = spawnSync(process.execPath, args, {
|
|
571
646
|
cwd: tenantRoot,
|
|
572
647
|
env,
|
|
573
648
|
stdio: input.stdio ?? "inherit"
|
|
574
649
|
});
|
|
575
|
-
return {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
650
|
+
return buildWorkflowResult("dev", tenantRoot, {
|
|
651
|
+
watch: input.watch === true,
|
|
652
|
+
background: false,
|
|
653
|
+
command: process.execPath,
|
|
654
|
+
args,
|
|
655
|
+
cwd: tenantRoot,
|
|
656
|
+
pid: null,
|
|
657
|
+
exitCode: result.status ?? 1,
|
|
658
|
+
runtime: {
|
|
659
|
+
mode: process.env.TREESEED_LOCAL_DEV_MODE ?? "cloudflare",
|
|
660
|
+
apiBaseUrl: process.env.TREESEED_API_BASE_URL ?? "http://127.0.0.1:3000",
|
|
661
|
+
webUrl: "http://127.0.0.1:8787"
|
|
662
|
+
},
|
|
663
|
+
readiness: readiness.readiness.local
|
|
664
|
+
});
|
|
588
665
|
});
|
|
589
666
|
} catch (error) {
|
|
590
667
|
toError("dev", error);
|
|
@@ -593,7 +670,7 @@ async function workflowDev(helpers, input = {}) {
|
|
|
593
670
|
async function workflowSave(helpers, input) {
|
|
594
671
|
try {
|
|
595
672
|
return withContextEnv(helpers.context.env, () => {
|
|
596
|
-
const tenantRoot = helpers.cwd();
|
|
673
|
+
const tenantRoot = resolveProjectRootOrThrow("save", helpers.cwd());
|
|
597
674
|
const message = ensureMessage("save", input.message, "a commit message");
|
|
598
675
|
const optionsHotfix = input.hotfix === true;
|
|
599
676
|
const root = workspaceRoot(tenantRoot);
|
|
@@ -616,45 +693,49 @@ async function workflowSave(helpers, input) {
|
|
|
616
693
|
if (input.verify !== false) {
|
|
617
694
|
runWorkspaceSavePreflight({ cwd: root });
|
|
618
695
|
}
|
|
619
|
-
|
|
620
|
-
|
|
696
|
+
const hadMeaningfulChanges = hasMeaningfulChanges(gitRoot);
|
|
697
|
+
let head = run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
|
|
698
|
+
let commitCreated = false;
|
|
699
|
+
if (hadMeaningfulChanges) {
|
|
700
|
+
run("git", ["add", "-A"], { cwd: gitRoot });
|
|
701
|
+
run("git", ["commit", "-m", message], { cwd: gitRoot });
|
|
702
|
+
head = run("git", ["rev-parse", "HEAD"], { cwd: gitRoot, capture: true }).trim();
|
|
703
|
+
commitCreated = true;
|
|
621
704
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
705
|
+
const branchSync = syncCurrentBranchToOrigin("save", gitRoot, branch);
|
|
706
|
+
let previewAction = { status: "skipped" };
|
|
707
|
+
if (beforeState.branchRole === "feature" && branch) {
|
|
708
|
+
if (input.preview === true) {
|
|
709
|
+
previewAction = {
|
|
710
|
+
status: beforeState.preview.enabled ? "refreshed" : "created",
|
|
711
|
+
details: deployBranchPreview(root, branch, helpers.context, { initialize: !beforeState.preview.enabled })
|
|
712
|
+
};
|
|
713
|
+
} else if (input.refreshPreview !== false && beforeState.preview.enabled) {
|
|
714
|
+
previewAction = {
|
|
715
|
+
status: "refreshed",
|
|
716
|
+
details: deployBranchPreview(root, branch, helpers.context, { initialize: false })
|
|
717
|
+
};
|
|
631
718
|
}
|
|
632
|
-
} catch {
|
|
633
|
-
const report = collectMergeConflictReport(gitRoot);
|
|
634
|
-
throw new TreeseedWorkflowError("save", "merge_conflict", formatMergeConflictReport(report, gitRoot, branch), {
|
|
635
|
-
details: { branch, report },
|
|
636
|
-
exitCode: 12
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
let previewRefresh = null;
|
|
640
|
-
if (input.refreshPreview !== false && beforeState.branchRole === "feature" && beforeState.preview.enabled && branch) {
|
|
641
|
-
previewRefresh = deployBranchPreview(root, branch, helpers.context, { initialize: false });
|
|
642
719
|
}
|
|
643
|
-
return
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
720
|
+
return buildWorkflowResult(
|
|
721
|
+
"save",
|
|
722
|
+
root,
|
|
723
|
+
{
|
|
647
724
|
branch,
|
|
648
725
|
scope,
|
|
649
726
|
hotfix: optionsHotfix,
|
|
650
727
|
message,
|
|
651
728
|
commitSha: head,
|
|
652
|
-
|
|
729
|
+
commitCreated,
|
|
730
|
+
noChanges: !hadMeaningfulChanges,
|
|
731
|
+
branchSync,
|
|
732
|
+
previewAction,
|
|
733
|
+
mergeConflict: null
|
|
653
734
|
},
|
|
654
|
-
|
|
735
|
+
createNextSteps([
|
|
655
736
|
branch === STAGING_BRANCH ? { operation: "release", reason: "Promote the validated staging branch into production.", input: { bump: "patch" } } : branch === PRODUCTION_BRANCH ? { operation: "status", reason: "Inspect production state after the explicit hotfix save." } : { operation: "stage", reason: "Merge the verified task branch into staging.", input: { message: "describe the resolution" } }
|
|
656
737
|
])
|
|
657
|
-
|
|
738
|
+
);
|
|
658
739
|
});
|
|
659
740
|
} catch (error) {
|
|
660
741
|
toError("save", error);
|
|
@@ -662,9 +743,13 @@ async function workflowSave(helpers, input) {
|
|
|
662
743
|
}
|
|
663
744
|
async function workflowClose(helpers, input) {
|
|
664
745
|
try {
|
|
665
|
-
return withContextEnv(helpers.context.env, () => {
|
|
666
|
-
const tenantRoot = helpers.cwd();
|
|
746
|
+
return withContextEnv(helpers.context.env, async () => {
|
|
747
|
+
const tenantRoot = resolveProjectRootOrThrow("close", helpers.cwd());
|
|
667
748
|
const message = ensureMessage("close", input.message, "a close reason");
|
|
749
|
+
const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "close", {
|
|
750
|
+
message,
|
|
751
|
+
autoSave: input.autoSave
|
|
752
|
+
});
|
|
668
753
|
const featureBranch = assertFeatureBranch(tenantRoot);
|
|
669
754
|
const repoDir = gitWorkflowRoot(tenantRoot);
|
|
670
755
|
assertCleanWorktree(tenantRoot);
|
|
@@ -675,21 +760,24 @@ async function workflowClose(helpers, input) {
|
|
|
675
760
|
if (input.deleteBranch !== false) {
|
|
676
761
|
deleteLocalBranch(repoDir, featureBranch);
|
|
677
762
|
}
|
|
678
|
-
return
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
763
|
+
return buildWorkflowResult(
|
|
764
|
+
"close",
|
|
765
|
+
tenantRoot,
|
|
766
|
+
{
|
|
682
767
|
branchName: featureBranch,
|
|
683
768
|
message,
|
|
769
|
+
autoSaved: autoSave.performed,
|
|
770
|
+
autoSaveResult: autoSave.save,
|
|
684
771
|
deprecatedTag,
|
|
685
772
|
previewCleanup,
|
|
686
773
|
remoteDeleted,
|
|
687
|
-
localDeleted: input.deleteBranch !== false
|
|
774
|
+
localDeleted: input.deleteBranch !== false,
|
|
775
|
+
finalBranch: currentBranch(repoDir) || STAGING_BRANCH
|
|
688
776
|
},
|
|
689
|
-
|
|
777
|
+
createNextSteps([
|
|
690
778
|
{ operation: "tasks", reason: "Inspect the remaining task branches after closing this one." }
|
|
691
779
|
])
|
|
692
|
-
|
|
780
|
+
);
|
|
693
781
|
});
|
|
694
782
|
} catch (error) {
|
|
695
783
|
toError("close", error);
|
|
@@ -697,12 +785,25 @@ async function workflowClose(helpers, input) {
|
|
|
697
785
|
}
|
|
698
786
|
async function workflowStage(helpers, input) {
|
|
699
787
|
try {
|
|
700
|
-
return withContextEnv(helpers.context.env, () => {
|
|
701
|
-
const tenantRoot = helpers.cwd();
|
|
788
|
+
return withContextEnv(helpers.context.env, async () => {
|
|
789
|
+
const tenantRoot = resolveProjectRootOrThrow("stage", helpers.cwd());
|
|
702
790
|
const message = ensureMessage("stage", input.message, "a resolution message");
|
|
791
|
+
const autoSave = await maybeAutoSaveCurrentTaskBranch(helpers, "stage", {
|
|
792
|
+
message,
|
|
793
|
+
autoSave: input.autoSave
|
|
794
|
+
});
|
|
703
795
|
const featureBranch = assertFeatureBranch(tenantRoot);
|
|
704
796
|
runWorkspaceSavePreflight({ cwd: tenantRoot });
|
|
705
|
-
|
|
797
|
+
let repoDir;
|
|
798
|
+
try {
|
|
799
|
+
repoDir = mergeCurrentBranchIntoStaging(tenantRoot, featureBranch);
|
|
800
|
+
} catch (error) {
|
|
801
|
+
const report = collectMergeConflictReport(gitWorkflowRoot(tenantRoot));
|
|
802
|
+
throw new TreeseedWorkflowError("stage", "merge_conflict", formatMergeConflictReport(report, gitWorkflowRoot(tenantRoot), STAGING_BRANCH), {
|
|
803
|
+
details: { branch: featureBranch, report, originalError: error instanceof Error ? error.message : String(error) },
|
|
804
|
+
exitCode: 12
|
|
805
|
+
});
|
|
806
|
+
}
|
|
706
807
|
const stagingWait = input.waitForStaging === false ? { status: "skipped", reason: "disabled" } : waitForStagingAutomation(repoDir);
|
|
707
808
|
const previewCleanup = input.deletePreview === false ? { performed: false } : destroyPreviewIfPresent(tenantRoot, featureBranch);
|
|
708
809
|
const deprecatedTag = createDeprecatedTaskTag(repoDir, featureBranch, `stage: ${message}`);
|
|
@@ -710,24 +811,27 @@ async function workflowStage(helpers, input) {
|
|
|
710
811
|
if (input.deleteBranch !== false) {
|
|
711
812
|
deleteLocalBranch(repoDir, featureBranch);
|
|
712
813
|
}
|
|
713
|
-
return
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
814
|
+
return buildWorkflowResult(
|
|
815
|
+
"stage",
|
|
816
|
+
tenantRoot,
|
|
817
|
+
{
|
|
717
818
|
branchName: featureBranch,
|
|
718
819
|
mergeTarget: STAGING_BRANCH,
|
|
719
820
|
message,
|
|
821
|
+
autoSaved: autoSave.performed,
|
|
822
|
+
autoSaveResult: autoSave.save,
|
|
720
823
|
deprecatedTag,
|
|
721
824
|
stagingWait,
|
|
722
825
|
previewCleanup,
|
|
723
826
|
remoteDeleted,
|
|
724
|
-
localDeleted: input.deleteBranch !== false
|
|
827
|
+
localDeleted: input.deleteBranch !== false,
|
|
828
|
+
finalBranch: currentBranch(repoDir) || STAGING_BRANCH
|
|
725
829
|
},
|
|
726
|
-
|
|
830
|
+
createNextSteps([
|
|
727
831
|
{ operation: "release", reason: "Promote the updated staging branch into production when ready.", input: { bump: "patch" } },
|
|
728
832
|
{ operation: "status", reason: "Inspect staging readiness after the task branch merge." }
|
|
729
833
|
])
|
|
730
|
-
|
|
834
|
+
);
|
|
731
835
|
});
|
|
732
836
|
} catch (error) {
|
|
733
837
|
toError("stage", error);
|
|
@@ -737,7 +841,7 @@ async function workflowRelease(helpers, input) {
|
|
|
737
841
|
try {
|
|
738
842
|
return withContextEnv(helpers.context.env, () => {
|
|
739
843
|
const level = input.bump ?? "patch";
|
|
740
|
-
const root =
|
|
844
|
+
const root = resolveProjectRootOrThrow("release", helpers.cwd());
|
|
741
845
|
const gitRoot = repoRoot(root);
|
|
742
846
|
prepareReleaseBranches(root);
|
|
743
847
|
applyTreeseedEnvironmentToProcess({ tenantRoot: root, scope: "staging", override: true });
|
|
@@ -750,23 +854,32 @@ async function workflowRelease(helpers, input) {
|
|
|
750
854
|
run("git", ["commit", "-m", `release: ${level} bump`], { cwd: gitRoot });
|
|
751
855
|
pushBranch(gitRoot, STAGING_BRANCH);
|
|
752
856
|
mergeStagingIntoMain(root);
|
|
857
|
+
const releasedCommit = run("git", ["rev-parse", PRODUCTION_BRANCH], { cwd: gitRoot, capture: true }).trim();
|
|
753
858
|
run("git", ["tag", "-a", rootVersion, "-m", `release: ${rootVersion}`], { cwd: gitRoot });
|
|
754
859
|
run("git", ["push", "origin", rootVersion], { cwd: gitRoot });
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
860
|
+
syncBranchWithOrigin(gitRoot, STAGING_BRANCH);
|
|
861
|
+
return buildWorkflowResult(
|
|
862
|
+
"release",
|
|
863
|
+
root,
|
|
864
|
+
{
|
|
759
865
|
level,
|
|
760
866
|
rootVersion,
|
|
761
867
|
releaseTag: rootVersion,
|
|
868
|
+
releasedCommit,
|
|
762
869
|
stagingBranch: STAGING_BRANCH,
|
|
763
870
|
productionBranch: PRODUCTION_BRANCH,
|
|
764
|
-
touchedPackages: [...plan.touched]
|
|
871
|
+
touchedPackages: [...plan.touched],
|
|
872
|
+
finalBranch: currentBranch(gitRoot) || STAGING_BRANCH,
|
|
873
|
+
pushStatus: {
|
|
874
|
+
stagingPushed: true,
|
|
875
|
+
productionPushed: true,
|
|
876
|
+
tagPushed: true
|
|
877
|
+
}
|
|
765
878
|
},
|
|
766
|
-
|
|
879
|
+
createNextSteps([
|
|
767
880
|
{ operation: "status", reason: "Inspect release readiness and production state after the promotion." }
|
|
768
881
|
])
|
|
769
|
-
|
|
882
|
+
);
|
|
770
883
|
});
|
|
771
884
|
} catch (error) {
|
|
772
885
|
toError("release", error);
|
|
@@ -832,6 +945,7 @@ export {
|
|
|
832
945
|
workflowConfig,
|
|
833
946
|
workflowDestroy,
|
|
834
947
|
workflowDev,
|
|
948
|
+
workflowExport,
|
|
835
949
|
workflowRelease,
|
|
836
950
|
workflowSave,
|
|
837
951
|
workflowStage,
|