@intelligo-dev/cli 1.0.0-beta.1 → 1.0.0-beta.2
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/bin.js +45 -19
- package/dist/bin.js.map +1 -1
- package/dist/commands/add.d.ts +1 -0
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +2 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/doctor.d.ts +14 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +75 -19
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/migrate.d.ts +97 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +124 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/upgrade-check.d.ts.map +1 -1
- package/dist/commands/upgrade-check.js +5 -2
- package/dist/commands/upgrade-check.js.map +1 -1
- package/dist/manifest.d.ts +8 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +4 -2
- package/dist/manifest.js.map +1 -1
- package/package.json +1 -1
- package/templates/app-scaffold/db-schema.ts.tpl +29 -0
- package/templates/app-scaffold/drizzle.config.ts.tpl +42 -0
- package/templates/app-scaffold/env.example.tpl +11 -0
- package/templates/app-scaffold/intelligo.ts.tpl +18 -3
- package/templates/app-scaffold/package.json.tpl +9 -1
- package/templates/app-scaffold/plans.ts.tpl +9 -1
- package/templates/app-scaffold/stripe-webhook-route.ts.tpl +24 -0
- package/templates/app-scaffold/theme-provider.tsx.tpl +6 -4
- package/templates/maintenance/maintenance-route.ts.tpl +110 -0
- package/templates/manifest.json +26 -4
- package/templates/registry-requires.json +139 -0
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import path from "node:path";
|
|
12
|
-
import { readCatalogue } from "./add.js";
|
|
12
|
+
import { readCatalogue, substitute } from "./add.js";
|
|
13
13
|
import { hashContents, readManifest } from "../manifest.js";
|
|
14
14
|
export function upgradeCheck(options) {
|
|
15
15
|
const manifest = readManifest(options.appRoot);
|
|
@@ -36,10 +36,13 @@ export function upgradeCheck(options) {
|
|
|
36
36
|
}
|
|
37
37
|
const localHash = hashContents(readFileSync(abs, "utf8"));
|
|
38
38
|
const customized = localHash !== file.hash;
|
|
39
|
+
// Compare against the template as it would be written for THIS
|
|
40
|
+
// app: the recorded hash is of substituted content, so the raw
|
|
41
|
+
// template never matched for any file carrying a placeholder.
|
|
39
42
|
const templateRel = targetToTemplate.get(file.path);
|
|
40
43
|
let templateChanged = false;
|
|
41
44
|
if (templateRel) {
|
|
42
|
-
const templateHash = hashContents(readFileSync(path.join(options.templatesDir, templateRel), "utf8"));
|
|
45
|
+
const templateHash = hashContents(substitute(readFileSync(path.join(options.templatesDir, templateRel), "utf8"), entry.variables));
|
|
43
46
|
templateChanged = templateHash !== file.hash;
|
|
44
47
|
}
|
|
45
48
|
report.items.push({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upgrade-check.js","sourceRoot":"","sources":["../../src/commands/upgrade-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"upgrade-check.js","sourceRoot":"","sources":["../../src/commands/upgrade-check.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,YAAY,EAAiB,MAAM,gBAAgB,CAAC;AA2B3E,MAAM,UAAU,YAAY,CAAC,OAA4B;IACvD,MAAM,QAAQ,GAAoB,YAAY,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAChE,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,EAAE,gBAAgB,EAAE,EAAE,EAAE,eAAe,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAClE,CAAC;IAED,MAAM,SAAS,GAAG,aAAa,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IACtD,MAAM,MAAM,GAAkB;QAC5B,gBAAgB,EAAE,EAAE;QACpB,eAAe,EAAE,EAAE;QACnB,KAAK,EAAE,EAAE;KACV,CAAC;IAEF,KAAK,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjE,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;QAChC,MAAM,CAAC,gBAAgB,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC,eAAe,CAAC;QACzD,IAAI,IAAI;YAAE,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC,eAAe,CAAC;QAEjE,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAC9B,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,CACvD,CAAC;QAEF,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;YAC/B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YAClD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBACrB,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;gBAClE,SAAS;YACX,CAAC;YAED,MAAM,SAAS,GAAG,YAAY,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;YAC1D,MAAM,UAAU,GAAG,SAAS,KAAK,IAAI,CAAC,IAAI,CAAC;YAE3C,+DAA+D;YAC/D,+DAA+D;YAC/D,8DAA8D;YAC9D,MAAM,WAAW,GAAG,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACpD,IAAI,eAAe,GAAG,KAAK,CAAC;YAC5B,IAAI,WAAW,EAAE,CAAC;gBAChB,MAAM,YAAY,GAAG,YAAY,CAC/B,UAAU,CACR,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,WAAW,CAAC,EAAE,MAAM,CAAC,EAClE,KAAK,CAAC,SAAS,CAChB,CACF,CAAC;gBACF,eAAe,GAAG,YAAY,KAAK,IAAI,CAAC,IAAI,CAAC;YAC/C,CAAC;YAED,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC;gBAChB,OAAO;gBACP,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,KAAK,EAAE,eAAe;oBACpB,CAAC,CAAC,UAAU;wBACV,CAAC,CAAC,UAAU;wBACZ,CAAC,CAAC,UAAU;oBACd,CAAC,CAAC,UAAU;wBACV,CAAC,CAAC,YAAY;wBACd,CAAC,CAAC,SAAS;aAChB,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,CAAgB;IAClD,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,mDAAmD,CAAC;IAC7D,CAAC;IAED,MAAM,KAAK,GAAyC;QAClD,OAAO,EAAE,cAAc;QACvB,QAAQ,EAAE,qDAAqD;QAC/D,QAAQ,EAAE,wDAAwD;QAClE,UAAU,EAAE,8BAA8B;QAC1C,OAAO,EAAE,kBAAkB;KAC5B,CAAC;IAEF,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACtE,MAAM,MAAM,GAAG,CAAC,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;QAC1C,KAAK,CAAC,IAAI,CACR,MAAM,IAAI,MAAM,KAAK,SAAS;YAC5B,CAAC,CAAC,GAAG,OAAO,KAAK,SAAS,MAAM,MAAM,EAAE;YACxC,CAAC,CAAC,GAAG,OAAO,KAAK,SAAS,EAAE,CAC/B,CAAC;QACF,KAAK,MAAM,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,KAAK,OAAO,CAAC,EAAE,CAAC;YAChE,KAAK,CAAC,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,CAAgB;IACnD,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7D,CAAC"}
|
package/dist/manifest.d.ts
CHANGED
|
@@ -25,6 +25,13 @@ export type GeneratedFile = {
|
|
|
25
25
|
export type FeatureEntry = {
|
|
26
26
|
templateVersion: string;
|
|
27
27
|
files: GeneratedFile[];
|
|
28
|
+
/**
|
|
29
|
+
* Placeholder substitutions the files were generated with
|
|
30
|
+
* (`__APP_NAME__` → "acme"). Recorded so an upgrade check can hash the
|
|
31
|
+
* template *as it would be written for this app* — hashing the raw
|
|
32
|
+
* template made every substituted file look permanently outdated.
|
|
33
|
+
*/
|
|
34
|
+
variables?: Record<string, string>;
|
|
28
35
|
};
|
|
29
36
|
export type Manifest = {
|
|
30
37
|
schemaVersion: 1;
|
|
@@ -54,5 +61,5 @@ export declare function inspectFeature(appRoot: string, entry: FeatureEntry): Fi
|
|
|
54
61
|
* template version, and the recorded hashes must then describe what is
|
|
55
62
|
* actually on disk.
|
|
56
63
|
*/
|
|
57
|
-
export declare function recordFeature(manifest: Manifest, feature: string, templateVersion: string, files: GeneratedFile[]): Manifest;
|
|
64
|
+
export declare function recordFeature(manifest: Manifest, feature: string, templateVersion: string, files: GeneratedFile[], variables?: Record<string, string>): Manifest;
|
|
58
65
|
//# sourceMappingURL=manifest.d.ts.map
|
package/dist/manifest.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,eAAO,MAAM,aAAa,4BAA4B,CAAC;AAEvD,MAAM,MAAM,aAAa,GAAG;IAC1B,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,aAAa,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,eAAO,MAAM,aAAa,4BAA4B,CAAC;AAEvD,MAAM,MAAM,aAAa,GAAG;IAC1B,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG;IACrB,aAAa,EAAE,CAAC,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACxC,CAAC;AAEF,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,wBAAgB,aAAa,CAAC,gBAAgB,EAAE,MAAM,GAAG,QAAQ,CAEhE;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CAI7D;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,GAAG,IAAI,CAKvE;AAED;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,YAAY,GAAG,SAAS,CAAC;AAE/D,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,SAAS,CAAC;CAClB,CAAC;AAEF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,GAAG,UAAU,CAS5E;AAED,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,KAAK,EAAE,YAAY,GAClB,UAAU,EAAE,CAEd;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,MAAM,EACf,eAAe,EAAE,MAAM,EACvB,KAAK,EAAE,aAAa,EAAE,EACtB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,QAAQ,CAUV"}
|
package/dist/manifest.js
CHANGED
|
@@ -53,12 +53,14 @@ export function inspectFeature(appRoot, entry) {
|
|
|
53
53
|
* template version, and the recorded hashes must then describe what is
|
|
54
54
|
* actually on disk.
|
|
55
55
|
*/
|
|
56
|
-
export function recordFeature(manifest, feature, templateVersion, files) {
|
|
56
|
+
export function recordFeature(manifest, feature, templateVersion, files, variables) {
|
|
57
57
|
return {
|
|
58
58
|
...manifest,
|
|
59
59
|
features: {
|
|
60
60
|
...manifest.features,
|
|
61
|
-
[feature]:
|
|
61
|
+
[feature]: variables
|
|
62
|
+
? { templateVersion, files, variables }
|
|
63
|
+
: { templateVersion, files },
|
|
62
64
|
},
|
|
63
65
|
};
|
|
64
66
|
}
|
package/dist/manifest.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,CAAC,MAAM,aAAa,GAAG,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAClE,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,CAAC,MAAM,aAAa,GAAG,yBAAyB,CAAC;AA2BvD,MAAM,UAAU,YAAY,CAAC,QAAgB;IAC3C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,gBAAwB;IACpD,OAAO,EAAE,aAAa,EAAE,CAAC,EAAE,gBAAgB,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;AAC9D,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;IAC/C,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAa,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,OAAe,EAAE,QAAkB;IAC/D,aAAa,CACX,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,aAAa,CAAC,EACjC,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CACzC,CAAC;AACJ,CAAC;AAeD,MAAM,UAAU,WAAW,CAAC,OAAe,EAAE,IAAmB;IAC9D,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAEnE,MAAM,OAAO,GAAG,YAAY,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,CAAC;IACxD,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,YAAY;KAC1D,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,OAAe,EACf,KAAmB;IAEnB,OAAO,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;AACzD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAC3B,QAAkB,EAClB,OAAe,EACf,eAAuB,EACvB,KAAsB,EACtB,SAAkC;IAElC,OAAO;QACL,GAAG,QAAQ;QACX,QAAQ,EAAE;YACR,GAAG,QAAQ,CAAC,QAAQ;YACpB,CAAC,OAAO,CAAC,EAAE,SAAS;gBAClB,CAAC,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE,SAAS,EAAE;gBACvC,CAAC,CAAC,EAAE,eAAe,EAAE,KAAK,EAAE;SAC/B;KACF,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tables this application owns.
|
|
3
|
+
*
|
|
4
|
+
* Everything the framework needs — users, workspaces, members, plans,
|
|
5
|
+
* subscriptions, credits, executions, audit events, notifications,
|
|
6
|
+
* conversations, documents — is `@intelligo-dev/core`'s and arrives
|
|
7
|
+
* with its own migrations. Define your product's tables here, scope
|
|
8
|
+
* them to a workspace, and reference framework tables by import:
|
|
9
|
+
*
|
|
10
|
+
* import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
11
|
+
* import { organization, users } from "@intelligo-dev/core/db/schema";
|
|
12
|
+
*
|
|
13
|
+
* export const reports = pgTable("reports", {
|
|
14
|
+
* id: text("id").primaryKey(),
|
|
15
|
+
* workspaceId: text("workspace_id")
|
|
16
|
+
* .notNull()
|
|
17
|
+
* .references(() => organization.id, { onDelete: "cascade" }),
|
|
18
|
+
* createdBy: text("created_by").references(() => users.id),
|
|
19
|
+
* createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* Then `pnpm db:generate` writes the migration into ./drizzle and
|
|
23
|
+
* `pnpm db:migrate` applies it after the framework's chain.
|
|
24
|
+
*
|
|
25
|
+
* Intelligo never reads these tables (ADR-0004: every table has one
|
|
26
|
+
* owner), so their shape is entirely yours.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineConfig } from "drizzle-kit";
|
|
2
|
+
import { config } from "dotenv";
|
|
3
|
+
|
|
4
|
+
// drizzle-kit does not load env files; Next.js does. Same precedence.
|
|
5
|
+
config({ path: ".env.local" });
|
|
6
|
+
config({ path: ".env" });
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Your application's migration chain — and only yours.
|
|
10
|
+
*
|
|
11
|
+
* The framework's tables (users, workspaces, plans, credits, executions,
|
|
12
|
+
* …) arrive inside `@intelligo-dev/core` together with their migration
|
|
13
|
+
* history, and `intelligo migrate` applies that chain. This config
|
|
14
|
+
* covers the tables you own: list their schema files here, generate
|
|
15
|
+
* with `pnpm db:generate`, apply with `pnpm db:migrate`.
|
|
16
|
+
*
|
|
17
|
+
* Two chains, one database, so they record themselves in different
|
|
18
|
+
* tables: the framework in drizzle's default `__drizzle_migrations`,
|
|
19
|
+
* yours in `__app_migrations`. Do not merge them — drizzle-kit applies
|
|
20
|
+
* by timestamp, and a framework migration published after you
|
|
21
|
+
* generated one of yours would be skipped silently.
|
|
22
|
+
*
|
|
23
|
+
* Your schema may reference framework tables (`references(() =>
|
|
24
|
+
* users.id)`); drizzle-kit emits the foreign key by name and does not
|
|
25
|
+
* try to create the framework's table.
|
|
26
|
+
*
|
|
27
|
+
* Do not point `drizzle-kit push` at this config: push diffs the whole
|
|
28
|
+
* database against the schema it sees, and this config deliberately
|
|
29
|
+
* sees only yours.
|
|
30
|
+
*/
|
|
31
|
+
export default defineConfig({
|
|
32
|
+
dialect: "postgresql",
|
|
33
|
+
schema: ["./lib/db/schema.ts"],
|
|
34
|
+
out: "./drizzle",
|
|
35
|
+
migrations: { table: "__app_migrations", schema: "drizzle" },
|
|
36
|
+
// The framework's columns are snake_case in Postgres and camelCase in
|
|
37
|
+
// TypeScript. Keep the same convention so joins read naturally.
|
|
38
|
+
casing: "snake_case",
|
|
39
|
+
dbCredentials: {
|
|
40
|
+
url: process.env.DATABASE_URL!,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Required — the app will not boot without these.
|
|
2
2
|
DATABASE_URL=
|
|
3
|
+
# Driver is chosen from the URL (Neon hosts get the WebSocket driver,
|
|
4
|
+
# everything else node-postgres). Force it with pg | neon-serverless.
|
|
5
|
+
# INTELLIGO_DB_DRIVER=
|
|
3
6
|
BETTER_AUTH_SECRET=
|
|
4
7
|
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
5
8
|
|
|
@@ -8,6 +11,14 @@ NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
|
8
11
|
# variable is here so CLI tooling can check it without booting the app.
|
|
9
12
|
INTELLIGO_BILLING_PRODUCT=__APP_SLUG__
|
|
10
13
|
|
|
14
|
+
# Stripe: the webhook endpoint at /api/webhooks/stripe verifies with this.
|
|
15
|
+
STRIPE_SECRET_KEY=
|
|
16
|
+
STRIPE_WEBHOOK_SECRET=
|
|
17
|
+
|
|
18
|
+
# Bearer token your scheduler sends to /api/cron/maintenance
|
|
19
|
+
# (`intelligo add maintenance`). At least 32 characters.
|
|
20
|
+
CRON_SECRET=
|
|
21
|
+
|
|
11
22
|
# Comma-separated emails allowed to reach the Intelligo admin console.
|
|
12
23
|
# Closed by default: with no value, nobody is a platform admin.
|
|
13
24
|
PLATFORM_ADMIN_EMAILS=
|
|
@@ -14,15 +14,17 @@ import "server-only";
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
|
-
|
|
17
|
+
reserveQuota,
|
|
18
18
|
recordTokenUsage,
|
|
19
19
|
releaseReservation,
|
|
20
|
+
findSettlementByRequestId,
|
|
20
21
|
} from "@intelligo-dev/billing";
|
|
21
22
|
import {
|
|
22
23
|
registerProductFeatures,
|
|
23
24
|
registerProductPlans,
|
|
24
25
|
setDefaultProductSlug,
|
|
25
26
|
} from "@intelligo-dev/billing/plans";
|
|
27
|
+
import { assertEnv } from "@intelligo-dev/core/env";
|
|
26
28
|
import { createExecutions } from "@intelligo-dev/executions";
|
|
27
29
|
|
|
28
30
|
import { PLANS, FEATURES } from "./plans";
|
|
@@ -44,6 +46,11 @@ export function composeIntelligo(): void {
|
|
|
44
46
|
if (composed) return;
|
|
45
47
|
composed = true;
|
|
46
48
|
|
|
49
|
+
// Fail on the first request rather than on the first query: a
|
|
50
|
+
// missing DATABASE_URL or auth secret is a configuration error, not
|
|
51
|
+
// something to discover deep inside a handler.
|
|
52
|
+
assertEnv();
|
|
53
|
+
|
|
47
54
|
setDefaultProductSlug(PRODUCT_SLUG);
|
|
48
55
|
registerProductPlans(PRODUCT_SLUG, PLANS);
|
|
49
56
|
registerProductFeatures(PRODUCT_SLUG, FEATURES);
|
|
@@ -54,9 +61,10 @@ export const executions = createExecutions({
|
|
|
54
61
|
// Passing requestId makes admission atomic: the worst-case cost is
|
|
55
62
|
// reserved in the same transaction that reads the balance, so
|
|
56
63
|
// concurrent requests cannot all pass.
|
|
57
|
-
const quota = await
|
|
64
|
+
const quota = await reserveQuota(workspaceId, { modelId: model, requestId });
|
|
58
65
|
return {
|
|
59
66
|
allowed: quota.allowed,
|
|
67
|
+
code: quota.code,
|
|
60
68
|
reason: quota.reason,
|
|
61
69
|
estimatedMnt: quota.estimatedMnt,
|
|
62
70
|
usingTrialCredits: quota.usingTrialCredits,
|
|
@@ -64,7 +72,9 @@ export const executions = createExecutions({
|
|
|
64
72
|
},
|
|
65
73
|
|
|
66
74
|
async settleUsage(s) {
|
|
67
|
-
|
|
75
|
+
// Returns what was charged and which pool funded it; the lifecycle
|
|
76
|
+
// records chargedMnt on the execution row.
|
|
77
|
+
return recordTokenUsage({
|
|
68
78
|
workspaceId: s.workspaceId,
|
|
69
79
|
userId: s.userId ?? "",
|
|
70
80
|
model: s.model ?? "unknown",
|
|
@@ -81,4 +91,9 @@ export const executions = createExecutions({
|
|
|
81
91
|
async releaseHold({ requestId }) {
|
|
82
92
|
await releaseReservation(requestId);
|
|
83
93
|
},
|
|
94
|
+
|
|
95
|
+
// Lets executions.reconcile() tell a settling row whose charge
|
|
96
|
+
// committed from one whose charge never happened.
|
|
97
|
+
findSettlement: ({ workspaceId, requestId }) =>
|
|
98
|
+
findSettlementByRequestId(workspaceId, requestId),
|
|
84
99
|
});
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
"dev": "next dev --turbopack --port 3000",
|
|
8
8
|
"build": "next build",
|
|
9
9
|
"start": "next start --port 3000",
|
|
10
|
-
"type-check": "tsc --noEmit"
|
|
10
|
+
"type-check": "tsc --noEmit",
|
|
11
|
+
"db:generate": "drizzle-kit generate",
|
|
12
|
+
"db:migrate": "intelligo migrate && drizzle-kit migrate",
|
|
13
|
+
"db:check": "intelligo migrate --check"
|
|
11
14
|
},
|
|
12
15
|
"dependencies": {
|
|
13
16
|
"@intelligo-dev/admin": "__INTELLIGO_DEP__",
|
|
@@ -16,10 +19,12 @@
|
|
|
16
19
|
"@intelligo-dev/billing": "__INTELLIGO_DEP__",
|
|
17
20
|
"@intelligo-dev/core": "__INTELLIGO_DEP__",
|
|
18
21
|
"@intelligo-dev/executions": "__INTELLIGO_DEP__",
|
|
22
|
+
"@intelligo-dev/jobs": "__INTELLIGO_DEP__",
|
|
19
23
|
"@intelligo-dev/ui": "__INTELLIGO_DEP__",
|
|
20
24
|
"@radix-ui/react-slot": "^1.2.4",
|
|
21
25
|
"class-variance-authority": "^0.7.1",
|
|
22
26
|
"clsx": "^2.1.1",
|
|
27
|
+
"drizzle-orm": "^0.45.1",
|
|
23
28
|
"lucide-react": "^0.563.0",
|
|
24
29
|
"next": "^16.2.3",
|
|
25
30
|
"next-intl": "^4.9.2",
|
|
@@ -31,10 +36,13 @@
|
|
|
31
36
|
"tailwind-merge": "^3.4.0"
|
|
32
37
|
},
|
|
33
38
|
"devDependencies": {
|
|
39
|
+
"@intelligo-dev/cli": "__INTELLIGO_DEP__",
|
|
34
40
|
"@tailwindcss/postcss": "^4",
|
|
35
41
|
"@types/node": "^22.0.0",
|
|
36
42
|
"@types/react": "^19.0.0",
|
|
37
43
|
"@types/react-dom": "^19.0.0",
|
|
44
|
+
"dotenv": "^16.4.7",
|
|
45
|
+
"drizzle-kit": "^0.31.8",
|
|
38
46
|
"tailwindcss": "^4",
|
|
39
47
|
"tw-animate-css": "^1.4.0",
|
|
40
48
|
"typescript": "^5.7.2"
|
|
@@ -37,7 +37,15 @@ export const PLANS: Record<string, PlanConfig> = {
|
|
|
37
37
|
},
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Which plans grant which feature. An unregistered feature is denied —
|
|
42
|
+
* a registry item whose `featureKey` is missing here returns 403 on
|
|
43
|
+
* every request, so add the key when you install the item.
|
|
44
|
+
*/
|
|
41
45
|
export const FEATURES: Record<string, readonly string[]> = {
|
|
42
46
|
assistant: ["free", "pro"],
|
|
47
|
+
// POST /api/chat (the `chat` registry item). Every plan, so a clean
|
|
48
|
+
// install can chat with no configuration; tighten to ["pro"] to put
|
|
49
|
+
// chat behind a paywall.
|
|
50
|
+
chat: ["free", "pro"],
|
|
43
51
|
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stripe webhook endpoint.
|
|
3
|
+
*
|
|
4
|
+
* Point Stripe at POST /api/webhooks/stripe and set
|
|
5
|
+
* STRIPE_WEBHOOK_SECRET. The handler verifies the signature, records
|
|
6
|
+
* every event in `finance_events` before processing it, runs the
|
|
7
|
+
* built-in handlers (checkout completed, invoice paid / failed,
|
|
8
|
+
* subscription updated / deleted), and answers 500 on a handler error
|
|
9
|
+
* so Stripe retries — see `createStripeWebhookHandler` in
|
|
10
|
+
* `@intelligo-dev/billing` for the exact contract.
|
|
11
|
+
*
|
|
12
|
+
* Pass `onEvent` to react to event types the framework does not.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createStripeWebhookHandler } from "@intelligo-dev/billing";
|
|
16
|
+
|
|
17
|
+
import { composeIntelligo } from "@/lib/intelligo";
|
|
18
|
+
|
|
19
|
+
const handler = createStripeWebhookHandler();
|
|
20
|
+
|
|
21
|
+
export async function POST(request: Request) {
|
|
22
|
+
composeIntelligo();
|
|
23
|
+
return handler(request);
|
|
24
|
+
}
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
* Theme provider — mounted by `app/[locale]/layout.tsx` so any theme
|
|
5
5
|
* switcher (e.g. the app-shell item's user menu) actually works.
|
|
6
6
|
*
|
|
7
|
-
* The
|
|
8
|
-
* path; installing app-shell
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* The `app-shell` registry item ships its own provider at this same
|
|
8
|
+
* path; installing app-shell (with --overwrite) replaces this starter
|
|
9
|
+
* copy, and the manifest will report the file as customized from then
|
|
10
|
+
* on — that is expected. Defaults: class-attribute strategy (Tailwind's
|
|
11
|
+
* `.dark` tokens), system preference honored, no transition flash on
|
|
12
|
+
* switch.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maintenance route — the loop the framework's comments assume exists.
|
|
3
|
+
*
|
|
4
|
+
* Mount a scheduler at GET /api/cron/maintenance with
|
|
5
|
+
* `Authorization: Bearer $CRON_SECRET` every few minutes. On Vercel,
|
|
6
|
+
* add an entry to `crons` in vercel.json with this path and a
|
|
7
|
+
* five-minute schedule (`0-59/5 * * * *`); Vercel sends the header
|
|
8
|
+
* itself.
|
|
9
|
+
*
|
|
10
|
+
* What one run does, and why each step exists:
|
|
11
|
+
*
|
|
12
|
+
* - **Reconcile stale executions.** A row still `running` or `settling`
|
|
13
|
+
* past the chat route's `maxDuration` is usage nobody finished
|
|
14
|
+
* accounting for. `executions.reconcile` confirms a charge the
|
|
15
|
+
* ledger already holds, re-runs settlement from the recorded usage,
|
|
16
|
+
* or fails an abandoned run and releases its hold.
|
|
17
|
+
* - **Drop expired reservations.** Admission already ignores them; this
|
|
18
|
+
* is table hygiene.
|
|
19
|
+
* - **Drop old rate-limit buckets.** Same.
|
|
20
|
+
* - **Expire trials and send reminders.** `hasActiveTrial` checks the
|
|
21
|
+
* end date inline, so admission is safe without this — feature
|
|
22
|
+
* access and the reminder emails are what it drives.
|
|
23
|
+
* - **Prune finished jobs** older than a week.
|
|
24
|
+
*
|
|
25
|
+
* Draining the job queue is NOT done here: `drain` needs your handlers,
|
|
26
|
+
* so give it its own route (or call it below once you have some).
|
|
27
|
+
*
|
|
28
|
+
* This file is yours. Add product maintenance to it freely.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { timingSafeEqual } from "node:crypto";
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
cleanupExpiredReservations,
|
|
35
|
+
cleanupRateLimitEntries,
|
|
36
|
+
processTrialExpirations,
|
|
37
|
+
} from "@intelligo-dev/billing";
|
|
38
|
+
import { findStaleExecutions } from "@intelligo-dev/executions";
|
|
39
|
+
import { pruneJobs } from "@intelligo-dev/jobs";
|
|
40
|
+
import { createLogger } from "@intelligo-dev/core/logger";
|
|
41
|
+
|
|
42
|
+
import { composeIntelligo, executions } from "@/lib/intelligo";
|
|
43
|
+
|
|
44
|
+
const log = createLogger("cron.maintenance");
|
|
45
|
+
|
|
46
|
+
export const maxDuration = 60;
|
|
47
|
+
|
|
48
|
+
/** Executions older than this and still non-terminal are reconciled. */
|
|
49
|
+
const STALE_AFTER_MS = 10 * 60_000;
|
|
50
|
+
/** Finished jobs older than this are pruned. */
|
|
51
|
+
const PRUNE_JOBS_AFTER_MS = 7 * 24 * 60 * 60_000;
|
|
52
|
+
|
|
53
|
+
/** Constant-time bearer comparison; length is compared too. */
|
|
54
|
+
function bearerMatches(header: string | null, secret: string): boolean {
|
|
55
|
+
if (!header?.startsWith("Bearer ")) return false;
|
|
56
|
+
const token = Buffer.from(header.slice("Bearer ".length));
|
|
57
|
+
const expected = Buffer.from(secret);
|
|
58
|
+
if (token.length !== expected.length) return false;
|
|
59
|
+
return timingSafeEqual(token, expected);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function GET(request: Request) {
|
|
63
|
+
const secret = process.env.CRON_SECRET;
|
|
64
|
+
if (!secret || secret.length < 32) {
|
|
65
|
+
log.error("CRON_SECRET missing or shorter than 32 chars — refusing");
|
|
66
|
+
return new Response("Forbidden", { status: 403 });
|
|
67
|
+
}
|
|
68
|
+
if (!bearerMatches(request.headers.get("authorization"), secret)) {
|
|
69
|
+
return new Response("Unauthorized", { status: 401 });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
composeIntelligo();
|
|
73
|
+
const errors: string[] = [];
|
|
74
|
+
const step = async <T>(name: string, run: () => Promise<T>): Promise<T | null> => {
|
|
75
|
+
try {
|
|
76
|
+
return await run();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
79
|
+
log.error(`${name} failed`, { error: message });
|
|
80
|
+
errors.push(`${name}: ${message}`);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const stale = await step("findStaleExecutions", () =>
|
|
86
|
+
findStaleExecutions(new Date(Date.now() - STALE_AFTER_MS))
|
|
87
|
+
);
|
|
88
|
+
const reconciled: Record<string, number> = {};
|
|
89
|
+
for (const row of stale ?? []) {
|
|
90
|
+
const result = await step(`reconcile ${row.id}`, () =>
|
|
91
|
+
executions.reconcile(row.id, { abandonRunningAfterMs: STALE_AFTER_MS })
|
|
92
|
+
);
|
|
93
|
+
if (result) reconciled[result.action] = (reconciled[result.action] ?? 0) + 1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const summary = {
|
|
97
|
+
staleExecutions: stale?.length ?? 0,
|
|
98
|
+
reconciled,
|
|
99
|
+
expiredReservationsDeleted: await step("cleanupExpiredReservations", cleanupExpiredReservations),
|
|
100
|
+
rateLimitEntriesDeleted: await step("cleanupRateLimitEntries", cleanupRateLimitEntries),
|
|
101
|
+
trials: await step("processTrialExpirations", processTrialExpirations),
|
|
102
|
+
jobsPruned: await step("pruneJobs", () =>
|
|
103
|
+
pruneJobs(new Date(Date.now() - PRUNE_JOBS_AFTER_MS))
|
|
104
|
+
),
|
|
105
|
+
errors,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
log.info("Maintenance run", summary);
|
|
109
|
+
return Response.json(summary, { status: errors.length ? 207 : 200 });
|
|
110
|
+
}
|
package/templates/manifest.json
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
"target": "app/usage/page.tsx"
|
|
9
9
|
}
|
|
10
10
|
],
|
|
11
|
-
"deprecated": "Superseded by the `usage` registry item
|
|
11
|
+
"deprecated": "Superseded by the `usage` registry item — install it with the shadcn CLI from the Intelligo registry instead; it ships a full page, components and thin actions as consumer-owned source."
|
|
12
12
|
},
|
|
13
13
|
"app-scaffold": {
|
|
14
|
-
"templateVersion": "1.
|
|
15
|
-
"description": "A new application: composition root, plans, an assistant route, config, next-intl i18n plumbing, and a registry-consuming shadcn/Tailwind setup",
|
|
14
|
+
"templateVersion": "1.7.0",
|
|
15
|
+
"description": "A new application: composition root, plans, an assistant route, a Stripe webhook endpoint, config, a drizzle config for the tables you own, next-intl i18n plumbing, and a registry-consuming shadcn/Tailwind setup",
|
|
16
16
|
"files": [
|
|
17
17
|
{
|
|
18
18
|
"template": "app-scaffold/package.json.tpl",
|
|
@@ -89,6 +89,18 @@
|
|
|
89
89
|
{
|
|
90
90
|
"template": "app-scaffold/messages-en-app.json.tpl",
|
|
91
91
|
"target": "messages/en/app.json"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
"template": "app-scaffold/drizzle.config.ts.tpl",
|
|
95
|
+
"target": "drizzle.config.ts"
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"template": "app-scaffold/db-schema.ts.tpl",
|
|
99
|
+
"target": "lib/db/schema.ts"
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"template": "app-scaffold/stripe-webhook-route.ts.tpl",
|
|
103
|
+
"target": "app/api/webhooks/stripe/route.ts"
|
|
92
104
|
}
|
|
93
105
|
]
|
|
94
106
|
},
|
|
@@ -101,7 +113,7 @@
|
|
|
101
113
|
"target": "app/billing/page.tsx"
|
|
102
114
|
}
|
|
103
115
|
],
|
|
104
|
-
"deprecated": "Superseded by the `pricing` and `billing-settings` registry items
|
|
116
|
+
"deprecated": "Superseded by the `pricing` and `billing-settings` registry items — install them with the shadcn CLI from the Intelligo registry instead."
|
|
105
117
|
},
|
|
106
118
|
"admin-page": {
|
|
107
119
|
"templateVersion": "1.0.0",
|
|
@@ -112,5 +124,15 @@
|
|
|
112
124
|
"target": "app/admin/page.tsx"
|
|
113
125
|
}
|
|
114
126
|
]
|
|
127
|
+
},
|
|
128
|
+
"maintenance": {
|
|
129
|
+
"templateVersion": "1.0.0",
|
|
130
|
+
"description": "A CRON_SECRET-gated GET /api/cron/maintenance that reconciles stale executions, drops expired reservations and rate-limit buckets, expires trials and prunes old jobs — schedule it every few minutes",
|
|
131
|
+
"files": [
|
|
132
|
+
{
|
|
133
|
+
"template": "maintenance/maintenance-route.ts.tpl",
|
|
134
|
+
"target": "app/api/cron/maintenance/route.ts"
|
|
135
|
+
}
|
|
136
|
+
]
|
|
115
137
|
}
|
|
116
138
|
}
|