deepline 0.3.213 → 0.3.215
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/bundling-sources/sdk/src/client.ts +122 -32
- package/dist/bundling-sources/sdk/src/compat.ts +2 -0
- package/dist/bundling-sources/sdk/src/deployment-protection.ts +19 -0
- package/dist/bundling-sources/sdk/src/http.ts +3 -7
- package/dist/bundling-sources/sdk/src/index.ts +7 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/types.ts +25 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +18 -0
- package/dist/bundling-sources/shared_libs/observability/scheduled-work-manual.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-registry.ts +33 -7
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +11 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-heartbeat.ts +3 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-payload-transport.ts +53 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-runtime-watchdog.ts +187 -6
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +12 -3
- package/dist/bundling-sources/shared_libs/play-runtime/test-runtime-seams.ts +5 -2
- package/dist/bundling-sources/shared_libs/plays/deploy/api-client.ts +40 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/manifest.ts +51 -28
- package/dist/bundling-sources/shared_libs/plays/deploy/monitor-files.ts +52 -0
- package/dist/bundling-sources/shared_libs/plays/deploy/run-deployment.ts +146 -19
- package/dist/bundling-sources/shared_libs/plays/settings-contract.ts +176 -0
- package/dist/cli/index.js +1046 -416
- package/dist/cli/index.mjs +894 -264
- package/dist/index.d.mts +165 -3
- package/dist/index.d.ts +165 -3
- package/dist/index.js +67 -35
- package/dist/index.mjs +67 -35
- package/dist/install-integrity.json +3 -0
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +3 -2
|
@@ -4,6 +4,8 @@ import type { PlayCompilerManifest } from '../compiler-manifest';
|
|
|
4
4
|
import {
|
|
5
5
|
DeployApiClient,
|
|
6
6
|
DeployApiError,
|
|
7
|
+
type DeploymentMonitorEntry,
|
|
8
|
+
type DeploymentMonitorOutcome,
|
|
7
9
|
type DeploymentPlayState,
|
|
8
10
|
type RegisterArtifactEntry,
|
|
9
11
|
} from './api-client';
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
DeeplineManifestError,
|
|
14
16
|
DEFAULT_MANIFEST_FILE,
|
|
15
17
|
} from './manifest';
|
|
18
|
+
import { monitorDeploymentSummary, readMonitorFiles } from './monitor-files';
|
|
16
19
|
import type { SerializedPlayDeployment } from './project-serializer';
|
|
17
20
|
import { resolveOwnedPlayReferenceName } from '../play-reference';
|
|
18
21
|
import type { BundledPlayFileResult } from '../bundling/index';
|
|
@@ -173,6 +176,7 @@ export type PlayDeploymentRunResult = {
|
|
|
173
176
|
| 'superseded'
|
|
174
177
|
| 'cancelled';
|
|
175
178
|
outcomes: PlayDeployOutcome[];
|
|
179
|
+
monitorOutcomes?: DeploymentMonitorOutcome[];
|
|
176
180
|
checkRunId?: number;
|
|
177
181
|
};
|
|
178
182
|
|
|
@@ -419,6 +423,11 @@ export async function runDeployment(
|
|
|
419
423
|
}
|
|
420
424
|
|
|
421
425
|
const buildSeconds = () => Math.max(0, (Date.now() - startedAt) / 1000);
|
|
426
|
+
let monitorOutcomes: DeploymentMonitorOutcome[] = [];
|
|
427
|
+
const monitorResults = () =>
|
|
428
|
+
monitorOutcomes.length > 0 ? { monitors: monitorOutcomes } : {};
|
|
429
|
+
const monitorRunResult = () =>
|
|
430
|
+
monitorOutcomes.length > 0 ? { monitorOutcomes } : {};
|
|
422
431
|
const finishFailed = async (input: {
|
|
423
432
|
error: string;
|
|
424
433
|
outcomes: PlayDeployOutcome[];
|
|
@@ -428,7 +437,7 @@ export async function runDeployment(
|
|
|
428
437
|
await setStatus({
|
|
429
438
|
status: 'failed',
|
|
430
439
|
error: input.error.slice(0, 2000),
|
|
431
|
-
results: { plays: input.outcomes },
|
|
440
|
+
results: { plays: input.outcomes, ...monitorResults() },
|
|
432
441
|
buildSeconds: buildSeconds(),
|
|
433
442
|
...(input.checkRunId ? { checkRunId: input.checkRunId } : {}),
|
|
434
443
|
});
|
|
@@ -453,6 +462,7 @@ export async function runDeployment(
|
|
|
453
462
|
return {
|
|
454
463
|
finalStatus: 'failed',
|
|
455
464
|
outcomes: input.outcomes,
|
|
465
|
+
...monitorRunResult(),
|
|
456
466
|
checkRunId: input.checkRunId,
|
|
457
467
|
};
|
|
458
468
|
};
|
|
@@ -462,13 +472,15 @@ export async function runDeployment(
|
|
|
462
472
|
): Promise<PlayDeploymentRunResult> => {
|
|
463
473
|
// Carried by the terminal status rather than per-play results so every
|
|
464
474
|
// reader — API, CLI, dashboard, alerting — sees it without parsing them.
|
|
465
|
-
const finalStatus = outcomes.some(
|
|
475
|
+
const finalStatus = [...outcomes, ...monitorOutcomes].some(
|
|
476
|
+
(outcome) => outcome.warning,
|
|
477
|
+
)
|
|
466
478
|
? ('succeeded_with_warnings' as const)
|
|
467
479
|
: ('succeeded' as const);
|
|
468
480
|
try {
|
|
469
481
|
await setStatus({
|
|
470
482
|
status: finalStatus,
|
|
471
|
-
results: { plays: outcomes },
|
|
483
|
+
results: { plays: outcomes, ...monitorResults() },
|
|
472
484
|
buildSeconds: buildSeconds(),
|
|
473
485
|
...(checkRun ? { checkRunId: checkRun.checkRunId } : {}),
|
|
474
486
|
});
|
|
@@ -494,6 +506,7 @@ export async function runDeployment(
|
|
|
494
506
|
return {
|
|
495
507
|
finalStatus,
|
|
496
508
|
outcomes,
|
|
509
|
+
...monitorRunResult(),
|
|
497
510
|
checkRunId: checkRun?.checkRunId,
|
|
498
511
|
};
|
|
499
512
|
};
|
|
@@ -615,8 +628,9 @@ export async function runDeployment(
|
|
|
615
628
|
await writeCheck({
|
|
616
629
|
conclusion: 'neutral',
|
|
617
630
|
title: `Deployment ${stopped}`,
|
|
618
|
-
summary:
|
|
619
|
-
'This deployment was stopped before it finished.
|
|
631
|
+
summary: monitorOutcomes.some((outcome) => outcome.jobId)
|
|
632
|
+
? 'This deployment was stopped before it finished. Monitor jobs already submitted may have changed live resources; inspect the jobs in its receipt.'
|
|
633
|
+
: 'This deployment was stopped before it finished. Nothing from it went live.',
|
|
620
634
|
}).catch(() => undefined);
|
|
621
635
|
throw revocation;
|
|
622
636
|
});
|
|
@@ -687,6 +701,7 @@ export async function runDeployment(
|
|
|
687
701
|
status: 'superseded',
|
|
688
702
|
error: `Branch ${deploymentBranch} is now at ${head?.slice(0, 12) ?? '<deleted>'}; this deployment's commit is no longer the head.`,
|
|
689
703
|
buildSeconds: buildSeconds(),
|
|
704
|
+
...(monitorOutcomes.length > 0 ? { results: monitorResults() } : {}),
|
|
690
705
|
...(head ? { observedHeadSha: head } : {}),
|
|
691
706
|
});
|
|
692
707
|
return { finalStatus: 'superseded' as const, outcomes: [] };
|
|
@@ -703,12 +718,25 @@ export async function runDeployment(
|
|
|
703
718
|
const manifestFile = target.manifestPath || DEFAULT_MANIFEST_FILE;
|
|
704
719
|
let manifest;
|
|
705
720
|
let playFiles: string[];
|
|
721
|
+
let monitorEntries: DeploymentMonitorEntry[] | undefined;
|
|
706
722
|
try {
|
|
707
723
|
manifest = await readDeeplineManifest(projectRoot, manifestFile);
|
|
708
724
|
playFiles = await discoverPlayFiles({
|
|
709
725
|
projectRoot,
|
|
710
726
|
globs: manifest.plays,
|
|
711
727
|
});
|
|
728
|
+
const discoveredMonitors = await readMonitorFiles({
|
|
729
|
+
projectRoot,
|
|
730
|
+
rootDirectory: target.rootDirectory,
|
|
731
|
+
globs: manifest.monitors,
|
|
732
|
+
});
|
|
733
|
+
if (!project && discoveredMonitors.length > 0) {
|
|
734
|
+
throw new DeeplineManifestError(
|
|
735
|
+
'Monitor Git deployment requires a linked repository. Deploy individual monitor files with deepline monitors deploy --file.',
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
monitorEntries =
|
|
739
|
+
project && discoveredMonitors.length > 0 ? discoveredMonitors : undefined;
|
|
712
740
|
} catch (error) {
|
|
713
741
|
if (error instanceof DeeplineManifestError) {
|
|
714
742
|
return await failWithCheck({
|
|
@@ -739,7 +767,29 @@ export async function runDeployment(
|
|
|
739
767
|
}
|
|
740
768
|
}
|
|
741
769
|
|
|
742
|
-
if (
|
|
770
|
+
if (monitorEntries !== undefined) {
|
|
771
|
+
const plan = await client.monitors('plan', monitorEntries);
|
|
772
|
+
monitorOutcomes = plan.monitors;
|
|
773
|
+
const failed = monitorOutcomes.filter(
|
|
774
|
+
(outcome) => outcome.status === 'failed',
|
|
775
|
+
);
|
|
776
|
+
if (failed.length > 0) {
|
|
777
|
+
return await failWithCheck({
|
|
778
|
+
title: 'Monitor validation failed',
|
|
779
|
+
summary: monitorDeploymentSummary(monitorOutcomes),
|
|
780
|
+
error: `${failed.length} monitor definition(s) failed validation.`,
|
|
781
|
+
outcomes: [],
|
|
782
|
+
annotations: failed.map((outcome) =>
|
|
783
|
+
annotationForFailure({
|
|
784
|
+
file: outcome.file,
|
|
785
|
+
message: outcome.error ?? 'Monitor validation failed.',
|
|
786
|
+
}),
|
|
787
|
+
),
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (playFiles.length === 0 && monitorOutcomes.length === 0) {
|
|
743
793
|
const checkRun = await postCheckOrNull({
|
|
744
794
|
conclusion: 'neutral',
|
|
745
795
|
title: 'No plays matched',
|
|
@@ -956,7 +1006,10 @@ export async function runDeployment(
|
|
|
956
1006
|
}
|
|
957
1007
|
|
|
958
1008
|
await setStatus({ status: 'publishing' }).catch(() => undefined);
|
|
959
|
-
const states =
|
|
1009
|
+
const states =
|
|
1010
|
+
ordered.length > 0
|
|
1011
|
+
? await client.playsState(ordered.map((play) => play.name))
|
|
1012
|
+
: { plays: [] };
|
|
960
1013
|
const stateByName = new Map<string, DeploymentPlayState>(
|
|
961
1014
|
states.plays.map((play) => [play.name, play]),
|
|
962
1015
|
);
|
|
@@ -1170,13 +1223,21 @@ export async function runDeployment(
|
|
|
1170
1223
|
// no validation signal (the deploy token dies with a terminal status).
|
|
1171
1224
|
const checkRun = await postCheckOrNull({
|
|
1172
1225
|
conclusion: 'success',
|
|
1173
|
-
title:
|
|
1226
|
+
title:
|
|
1227
|
+
monitorEntries !== undefined
|
|
1228
|
+
? `${ordered.length} play(s) validated and registered; ${monitorEntries.length} monitor(s) checked (not deployed)`
|
|
1229
|
+
: `${ordered.length} play${ordered.length === 1 ? '' : 's'} validated and registered (not deployed)`,
|
|
1174
1230
|
...(annotations.length > 0 ? { annotations } : {}),
|
|
1175
|
-
summary:
|
|
1176
|
-
|
|
1177
|
-
(
|
|
1178
|
-
|
|
1179
|
-
|
|
1231
|
+
summary: [
|
|
1232
|
+
outcomes
|
|
1233
|
+
.map(
|
|
1234
|
+
(outcome) =>
|
|
1235
|
+
`- \`${outcome.name}\`: ${outcome.status}${outcome.version !== undefined ? ` (v${outcome.version})` : ''}${outcome.sdkUpdated ? ' — SDK updated' : ''}${outcome.warning ? `\n - :warning: ${outcome.warning}` : ''}`,
|
|
1236
|
+
)
|
|
1237
|
+
.join('\n'),
|
|
1238
|
+
monitorDeploymentSummary(monitorOutcomes),
|
|
1239
|
+
]
|
|
1240
|
+
.filter(Boolean)
|
|
1180
1241
|
.join('\n'),
|
|
1181
1242
|
});
|
|
1182
1243
|
const result = await finishSucceeded(outcomes, checkRun);
|
|
@@ -1198,6 +1259,62 @@ export async function runDeployment(
|
|
|
1198
1259
|
if (superseded) return superseded;
|
|
1199
1260
|
}
|
|
1200
1261
|
|
|
1262
|
+
if (monitorEntries !== undefined) {
|
|
1263
|
+
monitorOutcomes = (await client.monitors('apply', monitorEntries))
|
|
1264
|
+
.monitors;
|
|
1265
|
+
// Provider changes are not atomic with Play promotion. Retain the exact
|
|
1266
|
+
// jobs in the deployment receipt before waiting or reporting a failure.
|
|
1267
|
+
await setStatus({
|
|
1268
|
+
status: 'publishing',
|
|
1269
|
+
results: { plays: outcomes, ...monitorResults() },
|
|
1270
|
+
});
|
|
1271
|
+
const deadline = Date.now() + 10 * 60_000;
|
|
1272
|
+
while (monitorOutcomes.some((outcome) => outcome.status === 'applying')) {
|
|
1273
|
+
if (Date.now() >= deadline) {
|
|
1274
|
+
throw new RetryableDeploymentError(
|
|
1275
|
+
'Monitor jobs are still reconciling. Retrying this deployment will resume the same jobs.',
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
1279
|
+
for (let index = 0; index < monitorOutcomes.length; index += 1) {
|
|
1280
|
+
const outcome = monitorOutcomes[index];
|
|
1281
|
+
if (outcome.status !== 'applying') continue;
|
|
1282
|
+
if (!outcome.jobId) {
|
|
1283
|
+
throw new Error(
|
|
1284
|
+
`Monitor ${outcome.key} has no reconciliation job.`,
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
const result = await client.monitorJob(outcome.jobId, outcome.key);
|
|
1288
|
+
const settled = result.monitors.find(
|
|
1289
|
+
(monitor) =>
|
|
1290
|
+
monitor.key === outcome.key && monitor.jobId === outcome.jobId,
|
|
1291
|
+
);
|
|
1292
|
+
if (!settled) {
|
|
1293
|
+
throw new Error(`Monitor job ${outcome.jobId} returned no result.`);
|
|
1294
|
+
}
|
|
1295
|
+
monitorOutcomes[index] = { ...outcome, ...settled };
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (
|
|
1299
|
+
monitorOutcomes.some(
|
|
1300
|
+
(outcome) =>
|
|
1301
|
+
outcome.status === 'failed' || outcome.status === 'planned',
|
|
1302
|
+
)
|
|
1303
|
+
) {
|
|
1304
|
+
return await failWithCheck({
|
|
1305
|
+
title: 'Monitor deployment failed',
|
|
1306
|
+
summary: monitorDeploymentSummary(monitorOutcomes),
|
|
1307
|
+
error:
|
|
1308
|
+
'Some monitor changes failed. Applied changes remain live; inspect the reported jobs before retrying.',
|
|
1309
|
+
outcomes,
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
// Monitor reconciliation may take minutes. Recheck the branch before
|
|
1313
|
+
// publishing Plays; a newer push must not be reverted by this one.
|
|
1314
|
+
const superseded = await supersedeIfHeadMoved();
|
|
1315
|
+
if (superseded) return { ...superseded, ...monitorRunResult() };
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1201
1318
|
if (toPromote.length > 0) {
|
|
1202
1319
|
try {
|
|
1203
1320
|
const promotion = await client.promote(toPromote);
|
|
@@ -1280,13 +1397,22 @@ export async function runDeployment(
|
|
|
1280
1397
|
|
|
1281
1398
|
const checkRun = await postCheckOrNull({
|
|
1282
1399
|
conclusion: 'success',
|
|
1283
|
-
title: `Deployed ${outcomes.filter((o) => o.status === 'promoted').length} play(s) (${outcomes.filter((o) => o.status === 'unchanged').length} unchanged)
|
|
1400
|
+
title: `Deployed ${outcomes.filter((o) => o.status === 'promoted').length} play(s) (${outcomes.filter((o) => o.status === 'unchanged').length} unchanged)${
|
|
1401
|
+
monitorEntries !== undefined
|
|
1402
|
+
? `; ${monitorOutcomes.filter((outcome) => outcome.status === 'applied').length} monitor(s) applied`
|
|
1403
|
+
: ''
|
|
1404
|
+
}`,
|
|
1284
1405
|
...(annotations.length > 0 ? { annotations } : {}),
|
|
1285
|
-
summary:
|
|
1286
|
-
|
|
1287
|
-
(
|
|
1288
|
-
|
|
1289
|
-
|
|
1406
|
+
summary: [
|
|
1407
|
+
outcomes
|
|
1408
|
+
.map(
|
|
1409
|
+
(outcome) =>
|
|
1410
|
+
`- \`${outcome.name}\`: ${outcome.status}${outcome.sdkUpdated ? ' — SDK updated' : ''}${outcome.warning ? `\n - :warning: ${outcome.warning}` : ''}`,
|
|
1411
|
+
)
|
|
1412
|
+
.join('\n'),
|
|
1413
|
+
monitorDeploymentSummary(monitorOutcomes),
|
|
1414
|
+
]
|
|
1415
|
+
.filter(Boolean)
|
|
1290
1416
|
.join('\n'),
|
|
1291
1417
|
});
|
|
1292
1418
|
return await finishSucceeded(outcomes, checkRun);
|
|
@@ -1342,6 +1468,7 @@ export async function runDeployment(
|
|
|
1342
1468
|
await setStatus({
|
|
1343
1469
|
status: 'failed',
|
|
1344
1470
|
error: message.slice(0, 2000),
|
|
1471
|
+
...(monitorOutcomes.length > 0 ? { results: monitorResults() } : {}),
|
|
1345
1472
|
buildSeconds: buildSeconds(),
|
|
1346
1473
|
...(checkRunId ? { checkRunId } : {}),
|
|
1347
1474
|
}).catch(() => undefined);
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/** Keep schema diagnostics readable in both interactive settings and the CLI. */
|
|
4
|
+
export function formatPlaySettingsError(error: unknown): string {
|
|
5
|
+
if (error instanceof z.ZodError)
|
|
6
|
+
return [...new Set(error.issues.map((issue) => issue.message))].join(' ');
|
|
7
|
+
return error instanceof Error ? error.message : String(error);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Location organizes Plays. Existing workspace permissions still apply. */
|
|
11
|
+
export const PLAY_LOCATION_ROOTS = [
|
|
12
|
+
{
|
|
13
|
+
value: 'person',
|
|
14
|
+
label: 'Personal',
|
|
15
|
+
description:
|
|
16
|
+
'The creator’s personal root, displayed as their name followed by “’s team”.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
value: 'team',
|
|
20
|
+
label: 'Team library',
|
|
21
|
+
description:
|
|
22
|
+
'The shared team library. Publishing moves personal locations here, keeping their folders.',
|
|
23
|
+
},
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
const folder = z
|
|
27
|
+
.string()
|
|
28
|
+
.min(1)
|
|
29
|
+
.max(128)
|
|
30
|
+
.refine(
|
|
31
|
+
(value) =>
|
|
32
|
+
value === value.trim() &&
|
|
33
|
+
value !== '.' &&
|
|
34
|
+
value !== '..' &&
|
|
35
|
+
!/[\\/\u0000-\u001f\u007f]/u.test(value),
|
|
36
|
+
'Folder names must be trimmed, cannot be . or .., and cannot contain slashes or control characters.',
|
|
37
|
+
);
|
|
38
|
+
export const playPathSchema = z
|
|
39
|
+
.union([
|
|
40
|
+
z.null(),
|
|
41
|
+
z
|
|
42
|
+
.object({
|
|
43
|
+
root: z.enum(['person', 'team']),
|
|
44
|
+
folders: z.array(folder).max(16).default([]),
|
|
45
|
+
})
|
|
46
|
+
.strict(),
|
|
47
|
+
])
|
|
48
|
+
.transform((path) =>
|
|
49
|
+
path?.root === 'person' && path.folders.length === 0 ? null : path,
|
|
50
|
+
);
|
|
51
|
+
export type PlayPath = z.output<typeof playPathSchema>;
|
|
52
|
+
|
|
53
|
+
export function parsePlayPath(value: string): PlayPath {
|
|
54
|
+
const [root, ...parts] = value.split('/');
|
|
55
|
+
if (!PLAY_LOCATION_ROOTS.some((option) => option.value === root)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
'Path must start with person or team. Example: person/Research/ICP.',
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
let folders: string[];
|
|
61
|
+
try {
|
|
62
|
+
folders = parts.map(decodeURIComponent);
|
|
63
|
+
} catch {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'Path contains an invalid percent escape. Encode a literal % as %25.',
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
return playPathSchema.parse({ root, folders });
|
|
69
|
+
}
|
|
70
|
+
export function formatPlayPath(path: PlayPath): string {
|
|
71
|
+
return path === null
|
|
72
|
+
? 'person'
|
|
73
|
+
: [path.root, ...path.folders.map(encodeURIComponent)].join('/');
|
|
74
|
+
}
|
|
75
|
+
export function personalPlayRootLabel(ownerName?: string | null): string {
|
|
76
|
+
const name = ownerName?.trim();
|
|
77
|
+
return name ? `${name.split(/\s+/)[0]}’s team` : 'Personal';
|
|
78
|
+
}
|
|
79
|
+
export function displayPlayPath(
|
|
80
|
+
path: PlayPath,
|
|
81
|
+
ownerName?: string | null,
|
|
82
|
+
): string {
|
|
83
|
+
return [
|
|
84
|
+
path?.root === 'team' ? 'Team library' : personalPlayRootLabel(ownerName),
|
|
85
|
+
...(path?.folders ?? []),
|
|
86
|
+
].join(' / ');
|
|
87
|
+
}
|
|
88
|
+
export function publishedPlayPath(path: PlayPath): PlayPath {
|
|
89
|
+
return { root: 'team', folders: path?.folders ?? [] };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** One registry drives validation, defaults, CLI keys/help and settings controls. */
|
|
93
|
+
export const PLAY_SETTING_FIELDS = {
|
|
94
|
+
path: {
|
|
95
|
+
schema: playPathSchema
|
|
96
|
+
.default(null)
|
|
97
|
+
.describe(
|
|
98
|
+
'Play location. Null means the creator’s personal root. Publishing moves it into Team library, preserving folders.',
|
|
99
|
+
),
|
|
100
|
+
label: 'Location',
|
|
101
|
+
description:
|
|
102
|
+
'Organize this Play in the creator’s personal root or Team library. Publishing moves it to Team library and keeps its folders. Workspace sharing permissions apply in both roots.',
|
|
103
|
+
get defaultValue(): PlayPath {
|
|
104
|
+
return this.schema.parse(undefined);
|
|
105
|
+
},
|
|
106
|
+
cli: {
|
|
107
|
+
valueSyntax: 'person[/folder/…] | team[/folder/…] | null',
|
|
108
|
+
examples: [
|
|
109
|
+
'person',
|
|
110
|
+
'person/Research/ICP',
|
|
111
|
+
'team',
|
|
112
|
+
'team/Research',
|
|
113
|
+
'null',
|
|
114
|
+
],
|
|
115
|
+
parse: (value: string) =>
|
|
116
|
+
value === 'null' ? null : parsePlayPath(value),
|
|
117
|
+
format: formatPlayPath,
|
|
118
|
+
},
|
|
119
|
+
ui: { control: 'play-location-picker' as const },
|
|
120
|
+
},
|
|
121
|
+
} as const;
|
|
122
|
+
export const playSettingsSchema = z.object(
|
|
123
|
+
Object.fromEntries(
|
|
124
|
+
Object.entries(PLAY_SETTING_FIELDS).map(([key, field]) => [
|
|
125
|
+
key,
|
|
126
|
+
field.schema,
|
|
127
|
+
]),
|
|
128
|
+
) as {
|
|
129
|
+
[K in keyof typeof PLAY_SETTING_FIELDS]: (typeof PLAY_SETTING_FIELDS)[K]['schema'];
|
|
130
|
+
},
|
|
131
|
+
).passthrough();
|
|
132
|
+
export const playSettingsPatchSchema = z
|
|
133
|
+
.object(
|
|
134
|
+
Object.fromEntries(
|
|
135
|
+
Object.entries(PLAY_SETTING_FIELDS).map(([key, field]) => [
|
|
136
|
+
key,
|
|
137
|
+
field.schema.unwrap().optional(),
|
|
138
|
+
]),
|
|
139
|
+
) as {
|
|
140
|
+
[K in keyof typeof PLAY_SETTING_FIELDS]: z.ZodOptional<
|
|
141
|
+
ReturnType<(typeof PLAY_SETTING_FIELDS)[K]['schema']['unwrap']>
|
|
142
|
+
>;
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
.strict()
|
|
146
|
+
.refine(
|
|
147
|
+
(patch) => Object.keys(patch).length > 0,
|
|
148
|
+
'Supply at least one setting.',
|
|
149
|
+
);
|
|
150
|
+
export type PlaySettings = {
|
|
151
|
+
[K in keyof typeof PLAY_SETTING_FIELDS]: z.output<
|
|
152
|
+
(typeof PLAY_SETTING_FIELDS)[K]['schema']
|
|
153
|
+
>;
|
|
154
|
+
};
|
|
155
|
+
export type PlaySettingsPatch = z.output<typeof playSettingsPatchSchema>;
|
|
156
|
+
/** Validate registered keys while preserving future keys in stored objects. */
|
|
157
|
+
export function parsePlaySettings(value: unknown): PlaySettings {
|
|
158
|
+
return playSettingsSchema.parse(value) as PlaySettings;
|
|
159
|
+
}
|
|
160
|
+
export const DEFAULT_PLAY_SETTINGS: PlaySettings = parsePlaySettings({});
|
|
161
|
+
export type PlaySettingsResult = {
|
|
162
|
+
playDefinitionId: string;
|
|
163
|
+
ownerUserId: string | null;
|
|
164
|
+
ownerName: string | null;
|
|
165
|
+
settings: PlaySettings;
|
|
166
|
+
};
|
|
167
|
+
export type PlaySettingsPathsPage = {
|
|
168
|
+
paths: Array<{
|
|
169
|
+
path: PlayPath;
|
|
170
|
+
ownerUserId: string | null;
|
|
171
|
+
ownerName: string | null;
|
|
172
|
+
playName: string;
|
|
173
|
+
}>;
|
|
174
|
+
cursor: string | null;
|
|
175
|
+
hasMore: boolean;
|
|
176
|
+
};
|