@mizchi/k1c 0.1.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.
Files changed (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +150 -0
  3. package/dist/canary/dispatcher-template.d.ts +17 -0
  4. package/dist/canary/dispatcher-template.js +42 -0
  5. package/dist/canary/effects-cloudflare.d.ts +9 -0
  6. package/dist/canary/effects-cloudflare.js +66 -0
  7. package/dist/canary/rollout-command.d.ts +15 -0
  8. package/dist/canary/rollout-command.js +92 -0
  9. package/dist/canary/runtime.d.ts +59 -0
  10. package/dist/canary/runtime.js +138 -0
  11. package/dist/canary/state-machine.d.ts +72 -0
  12. package/dist/canary/state-machine.js +161 -0
  13. package/dist/cli/args.d.ts +51 -0
  14. package/dist/cli/args.js +239 -0
  15. package/dist/cli/canary-integration.d.ts +11 -0
  16. package/dist/cli/canary-integration.js +101 -0
  17. package/dist/cli/format.d.ts +4 -0
  18. package/dist/cli/format.js +44 -0
  19. package/dist/cli/main.d.ts +3 -0
  20. package/dist/cli/main.js +158 -0
  21. package/dist/cli/run.d.ts +16 -0
  22. package/dist/cli/run.js +246 -0
  23. package/dist/manifest/lower.d.ts +22 -0
  24. package/dist/manifest/lower.js +913 -0
  25. package/dist/manifest/parse.d.ts +22 -0
  26. package/dist/manifest/parse.js +106 -0
  27. package/dist/manifest/schemas.d.ts +10359 -0
  28. package/dist/manifest/schemas.js +309 -0
  29. package/dist/manifest/types.d.ts +246 -0
  30. package/dist/manifest/types.js +12 -0
  31. package/dist/providers/configmap.d.ts +8 -0
  32. package/dist/providers/configmap.js +29 -0
  33. package/dist/providers/custom-domain.d.ts +11 -0
  34. package/dist/providers/custom-domain.js +120 -0
  35. package/dist/providers/d1-database.d.ts +9 -0
  36. package/dist/providers/d1-database.js +106 -0
  37. package/dist/providers/dispatch-namespace.d.ts +8 -0
  38. package/dist/providers/dispatch-namespace.js +100 -0
  39. package/dist/providers/dns-record.d.ts +14 -0
  40. package/dist/providers/dns-record.js +136 -0
  41. package/dist/providers/errors.d.ts +8 -0
  42. package/dist/providers/errors.js +64 -0
  43. package/dist/providers/hyperdrive.d.ts +27 -0
  44. package/dist/providers/hyperdrive.js +168 -0
  45. package/dist/providers/index.d.ts +6 -0
  46. package/dist/providers/index.js +36 -0
  47. package/dist/providers/kv-namespace.d.ts +8 -0
  48. package/dist/providers/kv-namespace.js +90 -0
  49. package/dist/providers/logpush-job.d.ts +17 -0
  50. package/dist/providers/logpush-job.js +181 -0
  51. package/dist/providers/queue.d.ts +10 -0
  52. package/dist/providers/queue.js +124 -0
  53. package/dist/providers/r2-bucket.d.ts +11 -0
  54. package/dist/providers/r2-bucket.js +98 -0
  55. package/dist/providers/registry.d.ts +9 -0
  56. package/dist/providers/registry.js +22 -0
  57. package/dist/providers/secret.d.ts +8 -0
  58. package/dist/providers/secret.js +30 -0
  59. package/dist/providers/types.d.ts +69 -0
  60. package/dist/providers/types.js +12 -0
  61. package/dist/providers/vectorize.d.ts +11 -0
  62. package/dist/providers/vectorize.js +110 -0
  63. package/dist/providers/worker.d.ts +106 -0
  64. package/dist/providers/worker.js +430 -0
  65. package/dist/providers/workflow.d.ts +10 -0
  66. package/dist/providers/workflow.js +103 -0
  67. package/dist/reconciler/apply.d.ts +10 -0
  68. package/dist/reconciler/apply.js +114 -0
  69. package/dist/reconciler/fake-provider.d.ts +48 -0
  70. package/dist/reconciler/fake-provider.js +83 -0
  71. package/dist/reconciler/plan.d.ts +6 -0
  72. package/dist/reconciler/plan.js +124 -0
  73. package/dist/reconciler/topo.d.ts +10 -0
  74. package/dist/reconciler/topo.js +53 -0
  75. package/dist/reconciler/types.d.ts +54 -0
  76. package/dist/reconciler/types.js +8 -0
  77. package/package.json +61 -0
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, watch } from 'node:fs/promises';
3
+ import process from 'node:process';
4
+ import { resolve as resolvePath } from 'node:path';
5
+ import Cloudflare from 'cloudflare';
6
+ import { parseArgs, USAGE } from "./args.js";
7
+ import { runApply, runDelete, runDescribe, runDiff, runGet } from "./run.js";
8
+ import { createDefaultRegistry } from "../providers/index.js";
9
+ import { runRolloutCommand } from "../canary/rollout-command.js";
10
+ import pkg from '../../package.json' with { type: 'json' };
11
+ const VERSION = pkg.version;
12
+ async function main() {
13
+ const parsed = parseArgs(process.argv.slice(2));
14
+ if (parsed.kind === 'help') {
15
+ process.stdout.write(USAGE);
16
+ return 0;
17
+ }
18
+ if (parsed.kind === 'version') {
19
+ process.stdout.write(`k1c ${VERSION}\n`);
20
+ return 0;
21
+ }
22
+ if (parsed.kind === 'error') {
23
+ process.stderr.write(`k1c: ${parsed.message}\n\n${USAGE}`);
24
+ return 2;
25
+ }
26
+ const accountId = process.env['K1C_ACCOUNT_ID'];
27
+ const apiToken = process.env['CLOUDFLARE_API_TOKEN'];
28
+ if (!accountId) {
29
+ process.stderr.write('K1C_ACCOUNT_ID is not set\n');
30
+ return 2;
31
+ }
32
+ if (!apiToken) {
33
+ process.stderr.write('CLOUDFLARE_API_TOKEN is not set\n');
34
+ return 2;
35
+ }
36
+ const cloudflare = new Cloudflare({ apiToken });
37
+ const ctx = {
38
+ cloudflare,
39
+ accountId,
40
+ namespace: 'default',
41
+ managedByLabel: 'k1c.io/managed-by=k1c',
42
+ signal: new AbortController().signal,
43
+ readFile: async (path) => readFile(path),
44
+ };
45
+ const deps = {
46
+ registry: createDefaultRegistry(),
47
+ providerCtx: ctx,
48
+ readManifest: async (path) => {
49
+ const buf = await readFile(path);
50
+ return buf.toString('utf-8');
51
+ },
52
+ out: (msg) => process.stdout.write(`${msg}\n`),
53
+ err: (msg) => process.stderr.write(`${msg}\n`),
54
+ };
55
+ if (parsed.kind === 'apply') {
56
+ return parsed.watch ? runApplyWatch(parsed, deps) : runApply(parsed, deps);
57
+ }
58
+ if (parsed.kind === 'diff')
59
+ return runDiff(parsed, deps);
60
+ if (parsed.kind === 'get')
61
+ return runGet(parsed, deps);
62
+ if (parsed.kind === 'describe')
63
+ return runDescribe(parsed, deps);
64
+ if (parsed.kind === 'delete')
65
+ return runDelete(parsed, deps);
66
+ if (parsed.kind === 'rollout')
67
+ return runRollout(parsed, cloudflare, accountId);
68
+ return 2;
69
+ }
70
+ async function runApplyWatch(args, deps) {
71
+ const filePath = resolvePath(args.file);
72
+ process.stdout.write(`(watching ${filePath} — initial apply)\n`);
73
+ await runApply(args, deps);
74
+ process.stdout.write(`\n(watching for changes; Ctrl-C to exit)\n`);
75
+ // Coalesce bursts of fs events; editors often emit multiple events per save.
76
+ let pending = null;
77
+ let inFlight = Promise.resolve();
78
+ const trigger = () => {
79
+ if (pending !== null)
80
+ clearTimeout(pending);
81
+ pending = setTimeout(() => {
82
+ pending = null;
83
+ inFlight = inFlight.then(async () => {
84
+ process.stdout.write(`\n(change detected → re-applying)\n`);
85
+ try {
86
+ await runApply(args, deps);
87
+ }
88
+ catch (e) {
89
+ process.stderr.write(`apply during watch failed: ${e instanceof Error ? e.message : String(e)}\n`);
90
+ }
91
+ });
92
+ }, 200);
93
+ };
94
+ try {
95
+ for await (const _event of watch(filePath)) {
96
+ trigger();
97
+ }
98
+ }
99
+ catch (e) {
100
+ if (e.code === 'ENOENT') {
101
+ process.stderr.write(`manifest no longer readable: ${filePath}\n`);
102
+ return 1;
103
+ }
104
+ throw e;
105
+ }
106
+ return 0;
107
+ }
108
+ async function runRollout(args, cloudflare, accountId) {
109
+ const stateKvId = await findStateKvId(cloudflare, accountId, args.dispatch);
110
+ if (stateKvId === null) {
111
+ process.stderr.write(`no rollout-state KV found for dispatch namespace "${args.dispatch}"; run \`k1c apply\` first\n`);
112
+ return 1;
113
+ }
114
+ const state = {
115
+ async read(key) {
116
+ try {
117
+ const resp = await cloudflare.kv.namespaces.values.get(stateKvId, key, {
118
+ account_id: accountId,
119
+ });
120
+ return await resp.text();
121
+ }
122
+ catch (e) {
123
+ if (isApi404(e))
124
+ return null;
125
+ throw e;
126
+ }
127
+ },
128
+ async write(key, value) {
129
+ await cloudflare.kv.namespaces.values.update(stateKvId, key, {
130
+ account_id: accountId,
131
+ value,
132
+ });
133
+ },
134
+ };
135
+ return runRolloutCommand({ subCommand: args.subCommand, target: args.target }, {
136
+ state,
137
+ out: (msg) => process.stdout.write(`${msg}\n`),
138
+ err: (msg) => process.stderr.write(`${msg}\n`),
139
+ });
140
+ }
141
+ async function findStateKvId(cf, accountId, dispatch) {
142
+ const expected = `k1c/rollout-state/${dispatch}`;
143
+ for await (const ns of cf.kv.namespaces.list({ account_id: accountId })) {
144
+ if (ns.title === expected)
145
+ return ns.id;
146
+ }
147
+ return null;
148
+ }
149
+ function isApi404(err) {
150
+ if (err === null || typeof err !== 'object')
151
+ return false;
152
+ return err.status === 404;
153
+ }
154
+ main().then((code) => process.exit(code), (err) => {
155
+ process.stderr.write(`fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
156
+ process.exit(1);
157
+ });
158
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1,16 @@
1
+ import type { ProviderRegistry } from '../providers/registry.ts';
2
+ import type { ProviderContext } from '../providers/types.ts';
3
+ import type { ApplyArgs, DeleteArgs, DescribeArgs, DiffArgs, GetArgs } from './args.ts';
4
+ export interface RunDeps {
5
+ readonly registry: ProviderRegistry;
6
+ readonly providerCtx: ProviderContext;
7
+ readonly readManifest: (path: string) => Promise<string>;
8
+ readonly out: (msg: string) => void;
9
+ readonly err: (msg: string) => void;
10
+ }
11
+ export declare function runApply(args: ApplyArgs, deps: RunDeps): Promise<number>;
12
+ export declare function runDiff(args: DiffArgs, deps: RunDeps): Promise<number>;
13
+ export declare function runGet(args: GetArgs, deps: RunDeps): Promise<number>;
14
+ export declare function runDescribe(args: DescribeArgs, deps: RunDeps): Promise<number>;
15
+ export declare function runDelete(args: DeleteArgs, deps: RunDeps): Promise<number>;
16
+ //# sourceMappingURL=run.d.ts.map
@@ -0,0 +1,246 @@
1
+ import { parseManifest } from "../manifest/parse.js";
2
+ import { lower } from "../manifest/lower.js";
3
+ import { plan } from "../reconciler/plan.js";
4
+ import { apply } from "../reconciler/apply.js";
5
+ import { NotFound } from "../providers/types.js";
6
+ import { formatPlan, formatReport } from "./format.js";
7
+ import { advanceCanaryRolloutsForApply } from "./canary-integration.js";
8
+ export async function runApply(args, deps) {
9
+ const loaded = await loadParsedAndDesired(args.file, deps);
10
+ if (loaded === null)
11
+ return 3;
12
+ const { parsed, desired } = loaded;
13
+ const planResult = await plan(desired, deps.registry, deps.providerCtx);
14
+ deps.out('[plan]');
15
+ deps.out(formatPlan(planResult));
16
+ if (args.dryRun) {
17
+ deps.out('');
18
+ deps.out('(dry-run: no changes applied)');
19
+ return 0;
20
+ }
21
+ let exitCode = 0;
22
+ if (planResult.operations.length > 0) {
23
+ deps.out('');
24
+ deps.out('[apply]');
25
+ const report = await apply(planResult, deps.registry, deps.providerCtx);
26
+ deps.out(formatReport(report));
27
+ if (report.failed > 0)
28
+ exitCode = 1;
29
+ }
30
+ if (exitCode === 0) {
31
+ try {
32
+ await advanceCanaryRolloutsForApply(parsed, desired, {
33
+ providerCtx: deps.providerCtx,
34
+ out: deps.out,
35
+ err: deps.err,
36
+ now: () => new Date(),
37
+ });
38
+ }
39
+ catch (e) {
40
+ deps.err(`canary advance failed: ${e instanceof Error ? e.message : String(e)}`);
41
+ exitCode = 1;
42
+ }
43
+ }
44
+ return exitCode;
45
+ }
46
+ export async function runDiff(args, deps) {
47
+ const loaded = await loadParsedAndDesired(args.file, deps);
48
+ if (loaded === null)
49
+ return 3;
50
+ const planResult = await plan(loaded.desired, deps.registry, deps.providerCtx);
51
+ if (args.output === 'json') {
52
+ deps.out(JSON.stringify({ operations: planResult.operations }, null, 2));
53
+ }
54
+ else {
55
+ deps.out('[plan]');
56
+ deps.out(formatPlan(planResult));
57
+ }
58
+ return 0;
59
+ }
60
+ export async function runGet(args, deps) {
61
+ if (!deps.registry.has(args.resourceKind)) {
62
+ deps.err(`unknown resource kind: ${args.resourceKind}`);
63
+ return 2;
64
+ }
65
+ const provider = deps.registry.get(args.resourceKind);
66
+ const rows = [];
67
+ try {
68
+ for await (const item of provider.list(deps.providerCtx)) {
69
+ if (args.namespace !== undefined && !item.label.startsWith(`${args.namespace}/`))
70
+ continue;
71
+ if (args.name !== undefined && !item.label.endsWith(`/${args.name}`))
72
+ continue;
73
+ rows.push({ label: item.label, nativeId: item.nativeId });
74
+ }
75
+ }
76
+ catch (e) {
77
+ deps.err(`get failed: ${e instanceof Error ? e.message : String(e)}`);
78
+ return 1;
79
+ }
80
+ if (args.output === 'json') {
81
+ deps.out(JSON.stringify({ kind: args.resourceKind, items: rows }, null, 2));
82
+ return 0;
83
+ }
84
+ if (rows.length === 0) {
85
+ deps.out(`(no ${args.resourceKind} resources found)`);
86
+ return 0;
87
+ }
88
+ const labelW = Math.max(8, ...rows.map((r) => r.label.length));
89
+ deps.out(`${pad('LABEL', labelW)} NATIVE_ID`);
90
+ for (const r of rows)
91
+ deps.out(`${pad(r.label, labelW)} ${r.nativeId}`);
92
+ return 0;
93
+ }
94
+ export async function runDescribe(args, deps) {
95
+ if (!deps.registry.has(args.resourceKind)) {
96
+ deps.err(`unknown resource kind: ${args.resourceKind}`);
97
+ return 2;
98
+ }
99
+ const provider = deps.registry.get(args.resourceKind);
100
+ const ns = args.namespace ?? 'default';
101
+ const targetLabel = `${ns}/${args.name}`;
102
+ let nativeId = null;
103
+ try {
104
+ for await (const item of provider.list(deps.providerCtx)) {
105
+ if (item.label === targetLabel) {
106
+ nativeId = item.nativeId;
107
+ break;
108
+ }
109
+ }
110
+ }
111
+ catch (e) {
112
+ deps.err(`describe failed during list: ${e instanceof Error ? e.message : String(e)}`);
113
+ return 1;
114
+ }
115
+ if (nativeId === null) {
116
+ deps.err(`${args.resourceKind} ${targetLabel} not found`);
117
+ return 1;
118
+ }
119
+ let props;
120
+ try {
121
+ props = await provider.read(deps.providerCtx, nativeId);
122
+ }
123
+ catch (e) {
124
+ deps.err(`describe failed during read: ${e instanceof Error ? e.message : String(e)}`);
125
+ return 1;
126
+ }
127
+ if (props === NotFound) {
128
+ deps.err(`${args.resourceKind} ${targetLabel} (nativeId=${nativeId}) was listed but read returned NotFound`);
129
+ return 1;
130
+ }
131
+ if (args.output === 'json') {
132
+ deps.out(JSON.stringify({
133
+ kind: args.resourceKind,
134
+ label: targetLabel,
135
+ nativeId,
136
+ properties: props,
137
+ }, null, 2));
138
+ return 0;
139
+ }
140
+ deps.out(`Kind: ${args.resourceKind}`);
141
+ deps.out(`Label: ${targetLabel}`);
142
+ deps.out(`NativeID: ${nativeId}`);
143
+ deps.out('Properties:');
144
+ deps.out(indent(JSON.stringify(props, null, 2), 2));
145
+ return 0;
146
+ }
147
+ export async function runDelete(args, deps) {
148
+ const loaded = await loadParsedAndDesired(args.file, deps);
149
+ if (loaded === null)
150
+ return 3;
151
+ const targets = [];
152
+ for (const d of loaded.desired) {
153
+ if (!args.cascade && (d.resourceType === 'R2Bucket' || d.resourceType === 'KVNamespace')) {
154
+ deps.out(`(skipping ${d.resourceType} ${d.label} — pass --cascade to delete user data)`);
155
+ continue;
156
+ }
157
+ targets.push({ resourceType: d.resourceType, label: d.label });
158
+ }
159
+ let succeeded = 0;
160
+ let failed = 0;
161
+ let skipped = 0;
162
+ for (const t of targets) {
163
+ if (!deps.registry.has(t.resourceType)) {
164
+ deps.err(`no provider for ${t.resourceType}; skipping ${t.label}`);
165
+ skipped += 1;
166
+ continue;
167
+ }
168
+ const provider = deps.registry.get(t.resourceType);
169
+ let nativeId = null;
170
+ try {
171
+ for await (const item of provider.list(deps.providerCtx)) {
172
+ if (item.label === t.label) {
173
+ nativeId = item.nativeId;
174
+ break;
175
+ }
176
+ }
177
+ }
178
+ catch (e) {
179
+ deps.err(`delete: list failed for ${t.resourceType} ${t.label}: ${e instanceof Error ? e.message : String(e)}`);
180
+ failed += 1;
181
+ continue;
182
+ }
183
+ if (nativeId === null) {
184
+ deps.out(`(${t.resourceType} ${t.label} not found in cluster, skipping)`);
185
+ skipped += 1;
186
+ continue;
187
+ }
188
+ try {
189
+ await provider.delete(deps.providerCtx, nativeId);
190
+ deps.out(`deleted ${t.resourceType} ${t.label} (${nativeId})`);
191
+ succeeded += 1;
192
+ }
193
+ catch (e) {
194
+ deps.err(`failed to delete ${t.resourceType} ${t.label}: ${e instanceof Error ? e.message : String(e)}`);
195
+ failed += 1;
196
+ }
197
+ }
198
+ deps.out(`summary: ${succeeded} deleted / ${failed} failed / ${skipped} skipped`);
199
+ return failed === 0 ? 0 : 1;
200
+ }
201
+ function pad(s, w) {
202
+ if (s.length >= w)
203
+ return s;
204
+ return s + ' '.repeat(w - s.length);
205
+ }
206
+ function indent(text, n) {
207
+ const prefix = ' '.repeat(n);
208
+ return text
209
+ .split('\n')
210
+ .map((line) => prefix + line)
211
+ .join('\n');
212
+ }
213
+ async function loadParsedAndDesired(file, deps) {
214
+ let yamlText;
215
+ try {
216
+ yamlText = await deps.readManifest(file);
217
+ }
218
+ catch (e) {
219
+ deps.err(`failed to read manifest ${file}: ${e instanceof Error ? e.message : String(e)}`);
220
+ return null;
221
+ }
222
+ let parsed;
223
+ try {
224
+ parsed = parseManifest(yamlText);
225
+ }
226
+ catch (e) {
227
+ deps.err(`manifest parse error: ${e instanceof Error ? e.message : String(e)}`);
228
+ return null;
229
+ }
230
+ for (const w of parsed.warnings) {
231
+ const where = w.ref ? `${w.ref.kind}/${w.ref.namespace}/${w.ref.name}` : '';
232
+ deps.err(`warning: ${where} ${w.message}`);
233
+ }
234
+ let lowered;
235
+ try {
236
+ lowered = await lower(parsed.resources, {
237
+ readFile: deps.providerCtx.readFile,
238
+ });
239
+ }
240
+ catch (e) {
241
+ deps.err(`lower error: ${e instanceof Error ? e.message : String(e)}`);
242
+ return null;
243
+ }
244
+ return { parsed: parsed.resources, desired: lowered.desired };
245
+ }
246
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1,22 @@
1
+ import type { K1cResource, ResourceRef } from './types.ts';
2
+ import type { DesiredResource } from '../reconciler/types.ts';
3
+ export declare class LowerError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ export interface LowerWarning {
7
+ readonly ref: ResourceRef;
8
+ readonly message: string;
9
+ }
10
+ export interface LowerResult {
11
+ readonly desired: ReadonlyArray<DesiredResource>;
12
+ readonly warnings: ReadonlyArray<LowerWarning>;
13
+ }
14
+ export interface LowerOptions {
15
+ /**
16
+ * Reads the bytes of a Worker entrypoint file. Defaults to fs/promises.readFile.
17
+ * Tests inject a stub to keep lower decoupled from disk I/O.
18
+ */
19
+ readonly readFile?: (path: string) => Promise<Uint8Array>;
20
+ }
21
+ export declare function lower(resources: ReadonlyArray<K1cResource>, options?: LowerOptions): Promise<LowerResult>;
22
+ //# sourceMappingURL=lower.d.ts.map