@norskvideo/ctl-dev-kit 0.1.96 → 0.1.98
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/build/flake.lock +18 -1
- package/build/flake.nix +5 -5
- package/create-product/cli.ts +61 -21
- package/create-product/create-product.ts +103 -18
- package/create-product/features/components.ts +632 -0
- package/create-product/features/dashboard.ts +270 -0
- package/create-product/features/frontend.ts +176 -0
- package/create-product/features/views.ts +97 -0
- package/create-product/{backend-turnkey.ts → turnkey.ts} +501 -82
- package/package.json +1 -1
package/build/flake.lock
CHANGED
|
@@ -16,9 +16,26 @@
|
|
|
16
16
|
"type": "github"
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
|
+
"nixpkgs-biome": {
|
|
20
|
+
"locked": {
|
|
21
|
+
"lastModified": 1785602060,
|
|
22
|
+
"narHash": "sha256-z7D96eESRM4CPV/XtwpwFn8IDdfLAmxz6lVWrGYXvR4=",
|
|
23
|
+
"owner": "NixOS",
|
|
24
|
+
"repo": "nixpkgs",
|
|
25
|
+
"rev": "a5cbcfe954791221bfffe2307f7d1a1bf61a871e",
|
|
26
|
+
"type": "github"
|
|
27
|
+
},
|
|
28
|
+
"original": {
|
|
29
|
+
"owner": "NixOS",
|
|
30
|
+
"repo": "nixpkgs",
|
|
31
|
+
"rev": "a5cbcfe954791221bfffe2307f7d1a1bf61a871e",
|
|
32
|
+
"type": "github"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
19
35
|
"root": {
|
|
20
36
|
"inputs": {
|
|
21
|
-
"nixpkgs": "nixpkgs"
|
|
37
|
+
"nixpkgs": "nixpkgs",
|
|
38
|
+
"nixpkgs-biome": "nixpkgs-biome"
|
|
22
39
|
}
|
|
23
40
|
}
|
|
24
41
|
},
|
package/build/flake.nix
CHANGED
|
@@ -36,13 +36,13 @@
|
|
|
36
36
|
|
|
37
37
|
# --- norsk-ctl channel pin -------------------------------------------
|
|
38
38
|
# The released daemon, one build per platform. Bump these together.
|
|
39
|
-
ctlVersion = "0.1.0-2026-
|
|
39
|
+
ctlVersion = "0.1.0-2026-09-15-81bbde8";
|
|
40
40
|
ctlBase = "https://s3.eu-west-1.amazonaws.com/norsk.video/norsk-ctl";
|
|
41
41
|
ctlAsset = {
|
|
42
|
-
"x86_64-linux" = { plat = "linux-x64"; hash = "sha256-
|
|
43
|
-
"aarch64-linux" = { plat = "linux-arm64"; hash = "sha256-
|
|
44
|
-
"aarch64-darwin" = { plat = "darwin-arm64"; hash = "sha256-
|
|
45
|
-
"x86_64-darwin" = { plat = "darwin-x64"; hash = "sha256-
|
|
42
|
+
"x86_64-linux" = { plat = "linux-x64"; hash = "sha256-6Zm1iLs29o89tav+TEDfLLdq2E+blku7DSxZVAqEwF0="; };
|
|
43
|
+
"aarch64-linux" = { plat = "linux-arm64"; hash = "sha256-ziH0YFLc8j7+xuYWArCU1JzWgq3NsFvfq67FqjlxdI8="; };
|
|
44
|
+
"aarch64-darwin" = { plat = "darwin-arm64"; hash = "sha256-A6uDgurkpYTwIdd7KwLv9oFNOLbGcoMgGLRW9SLuscw="; };
|
|
45
|
+
"x86_64-darwin" = { plat = "darwin-x64"; hash = "sha256-GzlT0Rg9t8sAG6e7Deu+DzSbfSxcr+WpVmyOgit2Q4g="; };
|
|
46
46
|
};
|
|
47
47
|
|
|
48
48
|
mkCtl = system:
|
package/create-product/cli.ts
CHANGED
|
@@ -5,18 +5,36 @@
|
|
|
5
5
|
// so the bin runs under bun (the fleet's only runtime).
|
|
6
6
|
import { existsSync, readdirSync } from "node:fs";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
|
-
import { createProduct, type ProductShape } from "./create-product.ts";
|
|
8
|
+
import { createProduct, type ProductFeature, type ProductShape, type StudioLibrary } from "./create-product.ts";
|
|
9
|
+
|
|
10
|
+
const SHAPES: ProductShape[] = ["backend-turnkey", "turnkey"];
|
|
9
11
|
|
|
10
12
|
const USAGE = `Usage:
|
|
11
13
|
ctl-dev-kit create-product --name <name> [options] <dir>
|
|
12
14
|
|
|
13
15
|
Options:
|
|
14
|
-
--shape <shape> Repo shape (default: backend-turnkey
|
|
16
|
+
--shape <shape> Repo shape — a preset over the features below (default: backend-turnkey)
|
|
17
|
+
backend-turnkey no features (reuters-shaped)
|
|
18
|
+
turnkey components,views,frontend (oracle-shaped)
|
|
19
|
+
--with <f,...> Add features on top of the shape's preset
|
|
20
|
+
--without <f,...> Drop features from the shape's preset
|
|
21
|
+
components custom Studio nodes packed into the template tar
|
|
22
|
+
views operator screen inside Studio's workflow view (needs components)
|
|
23
|
+
dashboard operator screen as a standalone SPA (needs components)
|
|
24
|
+
frontend the configure screen ctl iframes
|
|
15
25
|
--name <name> Product name, e.g. norsk-acme (lowercase, digits, hyphens)
|
|
16
26
|
--port <port> Local-dev backend port (default: 4323; container is always 4321)
|
|
17
27
|
--media-image <ref> Full media image ref (repo:tag); norsk-sdk npm pin = its tag
|
|
18
28
|
--studio-image <ref> Full studio image ref (repo:tag)
|
|
19
29
|
--studio-lib <ver> norsk-studio* npm pin (default: the studio image's tag)
|
|
30
|
+
--studio-libs <l,...> Studio component libraries beyond built-ins the workflow uses: alpha, beta
|
|
31
|
+
(e.g. alpha for processor.reasoning) — pinned, typed, and loaded by Studio
|
|
32
|
+
|
|
33
|
+
Examples:
|
|
34
|
+
ctl-dev-kit create-product --name norsk-acme ./acme # backend only
|
|
35
|
+
ctl-dev-kit create-product --name norsk-acme --shape turnkey ./acme # + component views
|
|
36
|
+
ctl-dev-kit create-product --name norsk-acme --shape turnkey \\
|
|
37
|
+
--with dashboard --without views ./acme # + a dashboard SPA instead
|
|
20
38
|
`;
|
|
21
39
|
|
|
22
40
|
function fail(message: string): never {
|
|
@@ -35,20 +53,21 @@ export function main(argv: string[]): void {
|
|
|
35
53
|
allowPositionals: true,
|
|
36
54
|
options: {
|
|
37
55
|
shape: { type: "string", default: "backend-turnkey" },
|
|
56
|
+
with: { type: "string" },
|
|
57
|
+
without: { type: "string" },
|
|
38
58
|
name: { type: "string" },
|
|
39
59
|
port: { type: "string" },
|
|
40
60
|
"media-image": { type: "string" },
|
|
41
61
|
"studio-image": { type: "string" },
|
|
42
62
|
"studio-lib": { type: "string" },
|
|
63
|
+
"studio-libs": { type: "string" },
|
|
43
64
|
},
|
|
44
65
|
});
|
|
45
66
|
const dir = positionals[0];
|
|
46
67
|
if (!dir) fail("missing <dir>.");
|
|
47
68
|
if (!values.name) fail("missing --name.");
|
|
48
|
-
if (values.shape
|
|
49
|
-
fail(
|
|
50
|
-
`unknown shape ${JSON.stringify(values.shape)} — shapes: backend-turnkey (turnkey and product are follow-ups).`,
|
|
51
|
-
);
|
|
69
|
+
if (!SHAPES.includes(values.shape as ProductShape)) {
|
|
70
|
+
fail(`unknown shape ${JSON.stringify(values.shape)} — shapes: ${SHAPES.join(", ")}.`);
|
|
52
71
|
}
|
|
53
72
|
if (existsSync(dir) && readdirSync(dir).length > 0) {
|
|
54
73
|
fail(`${dir} exists and is not empty — refusing to overwrite.`);
|
|
@@ -58,25 +77,46 @@ export function main(argv: string[]): void {
|
|
|
58
77
|
fail(`--port must be an integer in 1..65535, got ${JSON.stringify(values.port)}.`);
|
|
59
78
|
}
|
|
60
79
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
80
|
+
// Comma-separated so one flag carries a set; the resolver validates the names.
|
|
81
|
+
const csvList = <T extends string>(raw: string | undefined): T[] | undefined =>
|
|
82
|
+
raw === undefined
|
|
83
|
+
? undefined
|
|
84
|
+
: (raw
|
|
85
|
+
.split(",")
|
|
86
|
+
.map((f) => f.trim())
|
|
87
|
+
.filter(Boolean) as T[]);
|
|
88
|
+
|
|
89
|
+
// The resolver throws on a name, feature or combination it cannot honour;
|
|
90
|
+
// the operator asked for something impossible, not hit a bug, so report the
|
|
91
|
+
// reason and the usage rather than a stack trace.
|
|
92
|
+
let files: string[];
|
|
93
|
+
try {
|
|
94
|
+
({ files } = createProduct({
|
|
95
|
+
dir,
|
|
96
|
+
name: values.name,
|
|
97
|
+
shape: values.shape as ProductShape,
|
|
98
|
+
withFeatures: csvList<ProductFeature>(values.with),
|
|
99
|
+
withoutFeatures: csvList<ProductFeature>(values.without),
|
|
100
|
+
devPort,
|
|
101
|
+
mediaImage: values["media-image"],
|
|
102
|
+
studioImage: values["studio-image"],
|
|
103
|
+
studioLib: values["studio-lib"],
|
|
104
|
+
studioLibs: csvList<StudioLibrary>(values["studio-libs"]),
|
|
105
|
+
}));
|
|
106
|
+
} catch (err) {
|
|
107
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
108
|
+
}
|
|
70
109
|
|
|
71
110
|
console.log(`Generated ${files.length} files into ${dir} (shape: ${values.shape}).
|
|
72
111
|
|
|
73
|
-
Next steps (also in the generated CLAUDE.md):
|
|
74
|
-
1. cd ${dir} &&
|
|
75
|
-
2.
|
|
76
|
-
3. bun run
|
|
77
|
-
|
|
78
|
-
|
|
112
|
+
Next steps (also in the generated CLAUDE.md and README):
|
|
113
|
+
1. cd ${dir} && nix develop # the dev shell: pinned bun, biome and norsk-ctl
|
|
114
|
+
2. git init && bun install
|
|
115
|
+
3. UPDATE_SNAPSHOTS=1 bun run test:unit # bake the byte-snapshot fixtures, commit them
|
|
116
|
+
4. bun run check:drift && bun run docs:check && bun run test:unit && bun run typecheck && bun run lint
|
|
117
|
+
5. Replace the starter graph in shared/src/workflow/ with the real one,
|
|
79
118
|
keeping INVARIANTS.md and its rules tests in step.
|
|
119
|
+
6. bun run docs:handover # once a daemon has this product added: fills docs/handover.md from the launch truth
|
|
80
120
|
`);
|
|
81
121
|
}
|
|
82
122
|
|
|
@@ -4,17 +4,39 @@
|
|
|
4
4
|
// - the CONVENTION layer (this file): every file the drift gate checks,
|
|
5
5
|
// identical across shapes, with the per-repo fill-ins placed exactly where
|
|
6
6
|
// the gate masks or leaves structure free;
|
|
7
|
-
// - the SHAPE layer (
|
|
8
|
-
// manifests, manifest/composer/materials modules, the five test tiers
|
|
7
|
+
// - the SHAPE layer (turnkey.ts + features/): the source skeleton — package
|
|
8
|
+
// manifests, manifest/composer/materials modules, the five test tiers —
|
|
9
|
+
// with the optional workspaces (components, operator screen, configure
|
|
10
|
+
// screen) added by FEATURE, not by a second copy of the skeleton.
|
|
9
11
|
//
|
|
10
|
-
//
|
|
11
|
-
// implementations; the convention layer is shared verbatim.
|
|
12
|
+
// The convention layer is shared verbatim across every shape.
|
|
12
13
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
13
14
|
import { dirname, join } from "node:path";
|
|
14
|
-
import { backendTurnkey } from "./backend-turnkey.ts";
|
|
15
15
|
import { type Canon, loadAsset, loadCanon, PRODUCT_SENTINEL } from "./canon.ts";
|
|
16
|
+
import { turnkey } from "./turnkey.ts";
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
// A SHAPE is a preset over FEATURES, not a separate skeleton — the four
|
|
19
|
+
// product repos in the fleet are not a ladder (reuters: none; oracle:
|
|
20
|
+
// components + views; funke/probe: components + dashboard + frontend), so
|
|
21
|
+
// naming every combination would multiply shapes without adding anything.
|
|
22
|
+
export type ProductShape = "backend-turnkey" | "turnkey";
|
|
23
|
+
|
|
24
|
+
// - components the components/ workspace: custom Studio nodes packed into the
|
|
25
|
+
// product-template tar (all three UI surfaces need one).
|
|
26
|
+
// - views the operator screen AS component summary/fullscreen React
|
|
27
|
+
// rendered inside Studio's own workflow view (oracle).
|
|
28
|
+
// - dashboard the operator screen as a standalone SPA built into
|
|
29
|
+
// dashboards/<workflow>/ and served by Studio (funke, probe).
|
|
30
|
+
// - frontend the product-level configure screen ctl iframes at
|
|
31
|
+
// ui.configScreenUrl (funke, probe).
|
|
32
|
+
export type ProductFeature = "components" | "views" | "dashboard" | "frontend";
|
|
33
|
+
|
|
34
|
+
const ALL_FEATURES: readonly ProductFeature[] = ["components", "views", "dashboard", "frontend"];
|
|
35
|
+
|
|
36
|
+
const SHAPE_FEATURES: Record<ProductShape, readonly ProductFeature[]> = {
|
|
37
|
+
"backend-turnkey": [],
|
|
38
|
+
turnkey: ["components", "views", "frontend"],
|
|
39
|
+
};
|
|
18
40
|
|
|
19
41
|
export interface CreateProductOptions {
|
|
20
42
|
dir: string;
|
|
@@ -27,11 +49,23 @@ export interface CreateProductOptions {
|
|
|
27
49
|
mediaImage?: string;
|
|
28
50
|
/** Full studio image ref (repo:tag). */
|
|
29
51
|
studioImage?: string;
|
|
30
|
-
/** norsk-studio* npm pin; defaults to
|
|
31
|
-
*
|
|
52
|
+
/** norsk-studio* npm pin; defaults to `studioImage`'s bare tag when one is
|
|
53
|
+
* given, else to the default lib pin. */
|
|
32
54
|
studioLib?: string;
|
|
55
|
+
/** Features to add on top of the shape's preset (`--with`). */
|
|
56
|
+
withFeatures?: ProductFeature[];
|
|
57
|
+
/** Features to drop from the shape's preset (`--without`). */
|
|
58
|
+
withoutFeatures?: ProductFeature[];
|
|
59
|
+
/** Studio component libraries beyond built-ins the workflow uses (`--studio-libs`). */
|
|
60
|
+
studioLibs?: StudioLibrary[];
|
|
33
61
|
}
|
|
34
62
|
|
|
63
|
+
export const STUDIO_LIBRARIES = ["alpha", "beta"] as const;
|
|
64
|
+
export type StudioLibrary = (typeof STUDIO_LIBRARIES)[number];
|
|
65
|
+
|
|
66
|
+
/** `@norskvideo/norsk-studio-<lib>` — the npm package and the Studio server.library entry. */
|
|
67
|
+
export const studioPackage = (lib: StudioLibrary): string => `@norskvideo/norsk-studio-${lib}`;
|
|
68
|
+
|
|
35
69
|
export interface GeneratedFile {
|
|
36
70
|
path: string;
|
|
37
71
|
content: string;
|
|
@@ -47,6 +81,8 @@ export interface ShapeContext {
|
|
|
47
81
|
devPort: number;
|
|
48
82
|
pins: { mediaImage: string; studioImage: string; studioLib: string; mediaLib: string };
|
|
49
83
|
canon: Canon;
|
|
84
|
+
features: ReadonlySet<ProductFeature>;
|
|
85
|
+
studioLibs: StudioLibrary[];
|
|
50
86
|
}
|
|
51
87
|
|
|
52
88
|
export interface ShapeModule {
|
|
@@ -54,25 +90,69 @@ export interface ShapeModule {
|
|
|
54
90
|
claudeHead(ctx: ShapeContext): string;
|
|
55
91
|
claudeTail(ctx: ShapeContext): string;
|
|
56
92
|
rootTsconfigInclude: string[];
|
|
93
|
+
/** Everything below the drift-gated core block in .gitignore — the build
|
|
94
|
+
* output each enabled feature's workspace produces. */
|
|
95
|
+
gitignoreTail(ctx: ShapeContext): string;
|
|
57
96
|
}
|
|
58
97
|
|
|
98
|
+
// One skeleton, both shapes: the difference between them is entirely in ctx.features.
|
|
59
99
|
const SHAPES: Record<ProductShape, ShapeModule> = {
|
|
60
|
-
"backend-turnkey":
|
|
100
|
+
"backend-turnkey": turnkey,
|
|
101
|
+
turnkey,
|
|
61
102
|
};
|
|
62
103
|
|
|
63
|
-
// Same known-good coherent set the
|
|
104
|
+
// Same known-good coherent set the dev-kit's own package.json pins. A fresh
|
|
64
105
|
// repo's first upgrade-latest.yml run advances every one of these in lockstep.
|
|
65
|
-
const DEFAULT_MEDIA_IMAGE = "norskvideo/norsk:1.
|
|
66
|
-
const DEFAULT_STUDIO_IMAGE = "norskvideo/norsk-studio:1.
|
|
106
|
+
const DEFAULT_MEDIA_IMAGE = "norskvideo/norsk:1.2.1-2026-09-15-a41445b4";
|
|
107
|
+
const DEFAULT_STUDIO_IMAGE = "norskvideo/norsk-studio:1.29.0-2026-09-10-dcf40e7c";
|
|
108
|
+
// Studio's npm packages and image ship from different commits, so the default
|
|
109
|
+
// lib pin can't be derived from the default image's tag.
|
|
110
|
+
const DEFAULT_STUDIO_LIB = "1.29.0-2026-09-10-b21d9013";
|
|
67
111
|
|
|
68
112
|
const bareTag = (ref: string) => ref.slice(ref.lastIndexOf(":") + 1);
|
|
69
113
|
|
|
114
|
+
export function resolveFeatures(opts: CreateProductOptions): Set<ProductFeature> {
|
|
115
|
+
const known = (names: ProductFeature[] | undefined, flag: string): ProductFeature[] => {
|
|
116
|
+
for (const name of names ?? []) {
|
|
117
|
+
if (!ALL_FEATURES.includes(name)) {
|
|
118
|
+
throw new Error(`unknown feature ${JSON.stringify(name)} in ${flag} — features: ${ALL_FEATURES.join(", ")}.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return names ?? [];
|
|
122
|
+
};
|
|
123
|
+
const features = new Set<ProductFeature>(SHAPE_FEATURES[opts.shape] ?? []);
|
|
124
|
+
for (const name of known(opts.withoutFeatures, "--without")) features.delete(name);
|
|
125
|
+
for (const name of known(opts.withFeatures, "--with")) features.add(name);
|
|
126
|
+
|
|
127
|
+
// The template manifest's runtimeScreenUrl names ONE screen (/studio/ for
|
|
128
|
+
// views, /dashboard/<workflow>/ for the SPA), so a seed carrying both would
|
|
129
|
+
// ship one the operator can never reach.
|
|
130
|
+
if (features.has("views") && features.has("dashboard")) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
"features 'views' and 'dashboard' are two answers to the same question (where the operator screen lives) — pick one.",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
// Both operator screens read a component's state: views render inside the
|
|
136
|
+
// component itself, the SPA talks to it over /live/api/<id>/*.
|
|
137
|
+
const screen = features.has("views") ? "views" : features.has("dashboard") ? "dashboard" : undefined;
|
|
138
|
+
if (screen && !features.has("components")) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`feature ${JSON.stringify(screen)} needs 'components' — the operator screen reads a component's state.`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return features;
|
|
144
|
+
}
|
|
145
|
+
|
|
70
146
|
export function shapeContext(opts: CreateProductOptions): ShapeContext {
|
|
71
147
|
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(opts.name)) {
|
|
72
148
|
throw new Error(
|
|
73
149
|
`invalid product name ${JSON.stringify(opts.name)} — lowercase letters, digits, hyphens; 1-63 chars (it names the container image, npm scope, and manifest).`,
|
|
74
150
|
);
|
|
75
151
|
}
|
|
152
|
+
const unknownLibs = (opts.studioLibs ?? []).filter((lib) => !STUDIO_LIBRARIES.includes(lib));
|
|
153
|
+
if (unknownLibs.length > 0) {
|
|
154
|
+
throw new Error(`unknown studio libs ${unknownLibs.join(", ")} — known: ${STUDIO_LIBRARIES.join(", ")}.`);
|
|
155
|
+
}
|
|
76
156
|
const mediaImage = opts.mediaImage ?? DEFAULT_MEDIA_IMAGE;
|
|
77
157
|
const studioImage = opts.studioImage ?? DEFAULT_STUDIO_IMAGE;
|
|
78
158
|
return {
|
|
@@ -83,10 +163,12 @@ export function shapeContext(opts: CreateProductOptions): ShapeContext {
|
|
|
83
163
|
pins: {
|
|
84
164
|
mediaImage,
|
|
85
165
|
studioImage,
|
|
86
|
-
studioLib: opts.studioLib ?? bareTag(studioImage),
|
|
166
|
+
studioLib: opts.studioLib ?? (opts.studioImage ? bareTag(studioImage) : DEFAULT_STUDIO_LIB),
|
|
87
167
|
mediaLib: bareTag(mediaImage),
|
|
88
168
|
},
|
|
89
169
|
canon: loadCanon(),
|
|
170
|
+
features: resolveFeatures(opts),
|
|
171
|
+
studioLibs: STUDIO_LIBRARIES.filter((lib) => opts.studioLibs?.includes(lib)),
|
|
90
172
|
};
|
|
91
173
|
}
|
|
92
174
|
|
|
@@ -101,13 +183,16 @@ function conventionFiles(ctx: ShapeContext, shape: ShapeModule): GeneratedFile[]
|
|
|
101
183
|
{ path: "tsconfig.base.json", content: canon.tsconfigBase },
|
|
102
184
|
{ path: "dprint.json", content: canon.dprint },
|
|
103
185
|
{
|
|
186
|
+
// Hand-rolled rather than JSON.stringify'd: biome keeps a short array on
|
|
187
|
+
// one line, and a generated repo has to be lint-clean on its first run.
|
|
104
188
|
path: "tsconfig.json",
|
|
105
|
-
content:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
189
|
+
content: `{
|
|
190
|
+
"extends": "./tsconfig.base.json",
|
|
191
|
+
"include": [${shape.rootTsconfigInclude.map((entry) => JSON.stringify(entry)).join(", ")}]
|
|
192
|
+
}
|
|
193
|
+
`,
|
|
110
194
|
},
|
|
195
|
+
{ path: ".gitignore", content: `${canon.gitignoreCore}\n${shape.gitignoreTail(ctx)}` },
|
|
111
196
|
{ path: ".github/workflows/checks.yml", content: fillProduct(canon.checks) },
|
|
112
197
|
{ path: ".github/workflows/upgrade-latest.yml", content: fillProduct(canon.upgradeLatest) },
|
|
113
198
|
{ path: ".github/workflows/sync-dev-kit.yml", content: fillProduct(canon.syncDevKit) },
|